Implement backend API and database services in Docker setup

- Added a new `api` service for the NestJS backend, including health checks and dependencies on PostgreSQL, Redis, and MinIO.
- Introduced PostgreSQL and Redis services with health checks and configurations for data persistence.
- Added MinIO for S3-compatible object storage and a one-shot service to initialize required buckets.
- Updated the Nginx configuration to proxy requests to the new backend API and MinIO storage.
- Enhanced the Dockerfile to support the new API environment variables and configurations.
- Updated the `package.json` and `package-lock.json` to include new dependencies for QR code generation and other utilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dorian
2026-02-12 20:14:39 +00:00
parent f19fd6feef
commit cdd24a5def
478 changed files with 55355 additions and 529 deletions

View File

@@ -0,0 +1,190 @@
/**
* IndeeHub Self-Hosted API Service
*
* Dedicated API client for our self-hosted NestJS backend.
* Uses the /api/ proxy configured in nginx.
* Auth tokens are managed by nip98.service.ts.
*/
import axios, { type AxiosInstance } from 'axios'
import { indeehubApiConfig } from '../config/api.config'
import { nip98Service } from './nip98.service'
class IndeehubApiService {
private client: AxiosInstance
constructor() {
this.client = axios.create({
baseURL: indeehubApiConfig.baseURL,
timeout: indeehubApiConfig.timeout,
headers: {
'Content-Type': 'application/json',
},
})
// Attach JWT token from NIP-98 session
this.client.interceptors.request.use((config) => {
const token = nip98Service.accessToken
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Auto-refresh on 401
this.client.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const newToken = await nip98Service.refresh()
if (newToken) {
originalRequest.headers.Authorization = `Bearer ${newToken}`
return this.client(originalRequest)
}
}
return Promise.reject(error)
}
)
}
/**
* Generic typed GET request
*/
async get<T>(url: string): Promise<T> {
const response = await this.client.get<T>(url)
return response.data
}
/**
* Generic typed POST request
*/
async post<T>(url: string, data?: any): Promise<T> {
const response = await this.client.post<T>(url, data)
return response.data
}
/**
* Generic typed PATCH request
*/
async patch<T>(url: string, data?: any): Promise<T> {
const response = await this.client.patch<T>(url, data)
return response.data
}
/**
* Generic typed DELETE request
*/
async delete<T = void>(url: string): Promise<T> {
const response = await this.client.delete<T>(url)
return response.data
}
/**
* Check if the API is reachable
*/
async healthCheck(): Promise<boolean> {
try {
await this.client.get('/nostr-auth/health', { timeout: 5000 })
return true
} catch {
return false
}
}
/**
* Get all published projects
*/
async getProjects(filters?: {
status?: string
type?: string
genre?: string
limit?: number
offset?: number
}): Promise<any[]> {
const params = new URLSearchParams()
if (filters?.status) params.append('status', filters.status)
if (filters?.type) params.append('type', filters.type)
if (filters?.genre) params.append('genre', filters.genre)
if (filters?.limit) params.append('limit', String(filters.limit))
if (filters?.offset) params.append('offset', String(filters.offset))
const url = `/projects${params.toString() ? `?${params.toString()}` : ''}`
const response = await this.client.get(url)
return response.data
}
/**
* Get a single project by ID
*/
async getProject(id: string): Promise<any> {
const response = await this.client.get(`/projects/${id}`)
return response.data
}
/**
* Get streaming URL for a content item
* Returns different data based on deliveryMode (native vs partner)
*/
async getStreamingUrl(contentId: string): Promise<{
url: string
deliveryMode: 'native' | 'partner'
keyUrl?: string
drmToken?: string
}> {
const response = await this.client.get(`/contents/${contentId}/stream`)
return response.data
}
/**
* Get user's library items
*/
async getLibrary(): Promise<any[]> {
const response = await this.client.get('/library')
return response.data
}
/**
* Add a project to user's library
*/
async addToLibrary(projectId: string): Promise<any> {
const response = await this.client.post('/library', { projectId })
return response.data
}
/**
* Remove a project from user's library
*/
async removeFromLibrary(projectId: string): Promise<void> {
await this.client.delete(`/library/${projectId}`)
}
/**
* Get current user profile (requires auth)
*/
async getMe(): Promise<any> {
const response = await this.client.get('/auth/me')
return response.data
}
/**
* Get genres
*/
async getGenres(): Promise<any[]> {
const response = await this.client.get('/genres')
return response.data
}
/**
* Get the CDN URL for a storage path
*/
getCdnUrl(path: string): string {
if (!path) return ''
if (path.startsWith('http')) return path
if (path.startsWith('/')) return path
return `${indeehubApiConfig.cdnURL}/${path}`
}
}
export const indeehubApiService = new IndeehubApiService()