add service for directus; add setup for directus
This commit is contained in:
589
js/services/directus.js
Normal file
589
js/services/directus.js
Normal file
@@ -0,0 +1,589 @@
|
||||
/**
|
||||
* Directus API Service für dgray.io
|
||||
* Verbindet sich mit https://api.dgray.io/
|
||||
*/
|
||||
|
||||
const DIRECTUS_URL = 'https://api.dgray.io';
|
||||
|
||||
class DirectusService {
|
||||
constructor() {
|
||||
this.baseUrl = DIRECTUS_URL;
|
||||
this.accessToken = null;
|
||||
this.refreshToken = null;
|
||||
this.tokenExpiry = null;
|
||||
this.refreshTimeout = null;
|
||||
|
||||
this.loadTokens();
|
||||
}
|
||||
|
||||
// ==================== Token Management ====================
|
||||
|
||||
loadTokens() {
|
||||
const stored = localStorage.getItem('dgray_auth');
|
||||
if (stored) {
|
||||
try {
|
||||
const { accessToken, refreshToken, expiry } = JSON.parse(stored);
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
this.tokenExpiry = expiry;
|
||||
this.scheduleTokenRefresh();
|
||||
} catch (e) {
|
||||
this.clearTokens();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveTokens(accessToken, refreshToken, expiresIn) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
this.tokenExpiry = Date.now() + (expiresIn * 1000);
|
||||
|
||||
localStorage.setItem('dgray_auth', JSON.stringify({
|
||||
accessToken: this.accessToken,
|
||||
refreshToken: this.refreshToken,
|
||||
expiry: this.tokenExpiry
|
||||
}));
|
||||
|
||||
this.scheduleTokenRefresh();
|
||||
}
|
||||
|
||||
clearTokens() {
|
||||
this.accessToken = null;
|
||||
this.refreshToken = null;
|
||||
this.tokenExpiry = null;
|
||||
localStorage.removeItem('dgray_auth');
|
||||
|
||||
if (this.refreshTimeout) {
|
||||
clearTimeout(this.refreshTimeout);
|
||||
this.refreshTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleTokenRefresh() {
|
||||
if (this.refreshTimeout) {
|
||||
clearTimeout(this.refreshTimeout);
|
||||
}
|
||||
|
||||
if (!this.tokenExpiry || !this.refreshToken) return;
|
||||
|
||||
// Refresh 1 Minute vor Ablauf
|
||||
const refreshIn = this.tokenExpiry - Date.now() - 60000;
|
||||
|
||||
if (refreshIn > 0) {
|
||||
this.refreshTimeout = setTimeout(() => this.refreshSession(), refreshIn);
|
||||
} else if (this.refreshToken) {
|
||||
this.refreshSession();
|
||||
}
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return !!this.accessToken && (!this.tokenExpiry || Date.now() < this.tokenExpiry);
|
||||
}
|
||||
|
||||
// ==================== HTTP Methods ====================
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
if (this.accessToken) {
|
||||
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
|
||||
// Token abgelaufen - versuche Refresh
|
||||
if (response.status === 401 && this.refreshToken) {
|
||||
const refreshed = await this.refreshSession();
|
||||
if (refreshed) {
|
||||
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
return this.request(endpoint, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new DirectusError(response.status, error.errors?.[0]?.message || 'Request failed', error);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (error instanceof DirectusError) throw error;
|
||||
throw new DirectusError(0, 'Network error', { originalError: error });
|
||||
}
|
||||
}
|
||||
|
||||
async get(endpoint, params = {}) {
|
||||
const queryString = this.buildQueryString(params);
|
||||
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
||||
return this.request(url, { method: 'GET' });
|
||||
}
|
||||
|
||||
async post(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
async patch(endpoint, data) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
async delete(endpoint) {
|
||||
return this.request(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
buildQueryString(params) {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
|
||||
if (typeof value === 'object') {
|
||||
searchParams.set(key, JSON.stringify(value));
|
||||
} else {
|
||||
searchParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return searchParams.toString();
|
||||
}
|
||||
|
||||
// ==================== Authentication ====================
|
||||
|
||||
async login(email, password) {
|
||||
const response = await this.post('/auth/login', { email, password });
|
||||
|
||||
if (response.data) {
|
||||
this.saveTokens(
|
||||
response.data.access_token,
|
||||
response.data.refresh_token,
|
||||
response.data.expires
|
||||
);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
if (this.refreshToken) {
|
||||
try {
|
||||
await this.post('/auth/logout', { refresh_token: this.refreshToken });
|
||||
} catch (e) {
|
||||
// Ignorieren - Token wird trotzdem gelöscht
|
||||
}
|
||||
}
|
||||
this.clearTokens();
|
||||
}
|
||||
|
||||
async refreshSession() {
|
||||
if (!this.refreshToken) return false;
|
||||
|
||||
try {
|
||||
const response = await this.post('/auth/refresh', {
|
||||
refresh_token: this.refreshToken,
|
||||
mode: 'json'
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
this.saveTokens(
|
||||
response.data.access_token,
|
||||
response.data.refresh_token,
|
||||
response.data.expires
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
this.clearTokens();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async register(email, password, userData = {}) {
|
||||
return this.post('/users', {
|
||||
email,
|
||||
password,
|
||||
...userData,
|
||||
role: null // Wird durch Directus Flow/Policy gesetzt
|
||||
});
|
||||
}
|
||||
|
||||
async requestPasswordReset(email) {
|
||||
return this.post('/auth/password/request', { email });
|
||||
}
|
||||
|
||||
async resetPassword(token, password) {
|
||||
return this.post('/auth/password/reset', { token, password });
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
const response = await this.get('/users/me', {
|
||||
fields: ['id', 'email', 'first_name', 'last_name', 'avatar', 'role.name', 'status']
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateCurrentUser(data) {
|
||||
const response = await this.patch('/users/me', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ==================== Listings (Anzeigen) ====================
|
||||
|
||||
async getListings(options = {}) {
|
||||
const params = {
|
||||
fields: options.fields || [
|
||||
'*',
|
||||
'user_created.id',
|
||||
'user_created.first_name',
|
||||
'category.id',
|
||||
'category.name',
|
||||
'category.translations.*',
|
||||
'images.directus_files_id.*'
|
||||
],
|
||||
filter: options.filter || { status: { _eq: 'published' } },
|
||||
sort: options.sort || ['-date_created'],
|
||||
limit: options.limit || 20,
|
||||
page: options.page || 1,
|
||||
meta: 'total_count,filter_count'
|
||||
};
|
||||
|
||||
if (options.search) {
|
||||
params.search = options.search;
|
||||
}
|
||||
|
||||
const response = await this.get('/items/listings', params);
|
||||
return {
|
||||
items: response.data,
|
||||
meta: response.meta
|
||||
};
|
||||
}
|
||||
|
||||
async getListing(id) {
|
||||
const response = await this.get(`/items/listings/${id}`, {
|
||||
fields: [
|
||||
'*',
|
||||
'user_created.id',
|
||||
'user_created.first_name',
|
||||
'user_created.avatar',
|
||||
'category.*',
|
||||
'category.translations.*',
|
||||
'images.directus_files_id.*',
|
||||
'location.*'
|
||||
]
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createListing(data) {
|
||||
const response = await this.post('/items/listings', data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateListing(id, data) {
|
||||
const response = await this.patch(`/items/listings/${id}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteListing(id) {
|
||||
return this.delete(`/items/listings/${id}`);
|
||||
}
|
||||
|
||||
async getMyListings(options = {}) {
|
||||
return this.getListings({
|
||||
...options,
|
||||
filter: { user_created: { _eq: '$CURRENT_USER' } }
|
||||
});
|
||||
}
|
||||
|
||||
async searchListings(query, options = {}) {
|
||||
return this.getListings({
|
||||
...options,
|
||||
search: query
|
||||
});
|
||||
}
|
||||
|
||||
async getListingsByCategory(categoryId, options = {}) {
|
||||
return this.getListings({
|
||||
...options,
|
||||
filter: {
|
||||
status: { _eq: 'published' },
|
||||
category: { _eq: categoryId }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Categories (Kategorien) ====================
|
||||
|
||||
async getCategories() {
|
||||
const response = await this.get('/items/categories', {
|
||||
fields: ['*', 'translations.*', 'parent.*'],
|
||||
filter: { status: { _eq: 'published' } },
|
||||
sort: ['sort', 'name']
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getCategoryTree() {
|
||||
const categories = await this.getCategories();
|
||||
return this.buildCategoryTree(categories);
|
||||
}
|
||||
|
||||
buildCategoryTree(categories, parentId = null) {
|
||||
return categories
|
||||
.filter(cat => (cat.parent?.id || cat.parent) === parentId)
|
||||
.map(cat => ({
|
||||
...cat,
|
||||
children: this.buildCategoryTree(categories, cat.id)
|
||||
}));
|
||||
}
|
||||
|
||||
// ==================== Messages (Nachrichten) ====================
|
||||
|
||||
async getConversations() {
|
||||
const response = await this.get('/items/conversations', {
|
||||
fields: [
|
||||
'*',
|
||||
'listing.id',
|
||||
'listing.title',
|
||||
'listing.images.directus_files_id.id',
|
||||
'buyer.id',
|
||||
'buyer.first_name',
|
||||
'seller.id',
|
||||
'seller.first_name',
|
||||
'messages.*'
|
||||
],
|
||||
filter: {
|
||||
_or: [
|
||||
{ buyer: { _eq: '$CURRENT_USER' } },
|
||||
{ seller: { _eq: '$CURRENT_USER' } }
|
||||
]
|
||||
},
|
||||
sort: ['-date_updated']
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getConversation(id) {
|
||||
const response = await this.get(`/items/conversations/${id}`, {
|
||||
fields: [
|
||||
'*',
|
||||
'listing.*',
|
||||
'listing.images.directus_files_id.*',
|
||||
'buyer.*',
|
||||
'seller.*',
|
||||
'messages.*',
|
||||
'messages.sender.*'
|
||||
]
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async sendMessage(conversationId, content) {
|
||||
const response = await this.post('/items/messages', {
|
||||
conversation: conversationId,
|
||||
content
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async startConversation(listingId, message) {
|
||||
const response = await this.post('/items/conversations', {
|
||||
listing: listingId,
|
||||
messages: {
|
||||
create: [{ content: message }]
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ==================== Favorites (Favoriten) ====================
|
||||
|
||||
async getFavorites() {
|
||||
const response = await this.get('/items/favorites', {
|
||||
fields: ['*', 'listing.*', 'listing.images.directus_files_id.*'],
|
||||
filter: { user: { _eq: '$CURRENT_USER' } }
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async addFavorite(listingId) {
|
||||
const response = await this.post('/items/favorites', {
|
||||
listing: listingId
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async removeFavorite(favoriteId) {
|
||||
return this.delete(`/items/favorites/${favoriteId}`);
|
||||
}
|
||||
|
||||
async isFavorite(listingId) {
|
||||
const response = await this.get('/items/favorites', {
|
||||
filter: {
|
||||
user: { _eq: '$CURRENT_USER' },
|
||||
listing: { _eq: listingId }
|
||||
},
|
||||
limit: 1
|
||||
});
|
||||
return response.data.length > 0 ? response.data[0] : null;
|
||||
}
|
||||
|
||||
// ==================== Reports (Meldungen) ====================
|
||||
|
||||
async reportListing(listingId, reason, details = '') {
|
||||
const response = await this.post('/items/reports', {
|
||||
listing: listingId,
|
||||
reason,
|
||||
details
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async reportUser(userId, reason, details = '') {
|
||||
const response = await this.post('/items/reports', {
|
||||
reported_user: userId,
|
||||
reason,
|
||||
details
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ==================== Files (Dateien/Bilder) ====================
|
||||
|
||||
async uploadFile(file, options = {}) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
if (options.folder) {
|
||||
formData.append('folder', options.folder);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/files`, {
|
||||
method: 'POST',
|
||||
headers: this.accessToken ? { 'Authorization': `Bearer ${this.accessToken}` } : {},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new DirectusError(response.status, 'Upload failed', error);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async uploadMultipleFiles(files, options = {}) {
|
||||
const uploads = Array.from(files).map(file => this.uploadFile(file, options));
|
||||
return Promise.all(uploads);
|
||||
}
|
||||
|
||||
getFileUrl(fileId, options = {}) {
|
||||
if (!fileId) return null;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options.width) params.set('width', options.width);
|
||||
if (options.height) params.set('height', options.height);
|
||||
if (options.fit) params.set('fit', options.fit);
|
||||
if (options.quality) params.set('quality', options.quality);
|
||||
if (options.format) params.set('format', options.format);
|
||||
|
||||
const queryString = params.toString();
|
||||
return `${this.baseUrl}/assets/${fileId}${queryString ? '?' + queryString : ''}`;
|
||||
}
|
||||
|
||||
getThumbnailUrl(fileId, size = 300) {
|
||||
return this.getFileUrl(fileId, { width: size, height: size, fit: 'cover' });
|
||||
}
|
||||
|
||||
// ==================== Search ====================
|
||||
|
||||
async globalSearch(query, options = {}) {
|
||||
const [listings, categories] = await Promise.all([
|
||||
this.searchListings(query, { limit: options.listingLimit || 10 }),
|
||||
this.get('/items/categories', {
|
||||
search: query,
|
||||
limit: options.categoryLimit || 5
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
listings: listings.items,
|
||||
categories: categories.data
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Stats / Dashboard ====================
|
||||
|
||||
async getUserStats() {
|
||||
const [listings, favorites, conversations] = await Promise.all([
|
||||
this.get('/items/listings', {
|
||||
filter: { user_created: { _eq: '$CURRENT_USER' } },
|
||||
aggregate: { count: '*' }
|
||||
}),
|
||||
this.get('/items/favorites', {
|
||||
filter: { user: { _eq: '$CURRENT_USER' } },
|
||||
aggregate: { count: '*' }
|
||||
}),
|
||||
this.get('/items/conversations', {
|
||||
filter: {
|
||||
_or: [
|
||||
{ buyer: { _eq: '$CURRENT_USER' } },
|
||||
{ seller: { _eq: '$CURRENT_USER' } }
|
||||
]
|
||||
},
|
||||
aggregate: { count: '*' }
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
listingsCount: listings.data?.[0]?.count || 0,
|
||||
favoritesCount: favorites.data?.[0]?.count || 0,
|
||||
conversationsCount: conversations.data?.[0]?.count || 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DirectusError extends Error {
|
||||
constructor(status, message, data = {}) {
|
||||
super(message);
|
||||
this.name = 'DirectusError';
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
isAuthError() {
|
||||
return this.status === 401 || this.status === 403;
|
||||
}
|
||||
|
||||
isNotFound() {
|
||||
return this.status === 404;
|
||||
}
|
||||
|
||||
isValidationError() {
|
||||
return this.status === 400;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton Export
|
||||
export const directus = new DirectusService();
|
||||
export { DirectusError };
|
||||
Reference in New Issue
Block a user