diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d433932e9..aac1f2745 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,6 @@ jobs: clock-web: ${{ steps.changes.outputs.clock-web }} contacts-backend: ${{ steps.changes.outputs.contacts-backend }} contacts-web: ${{ steps.changes.outputs.contacts-web }} - presi-backend: ${{ steps.changes.outputs.presi-backend }} presi-web: ${{ steps.changes.outputs.presi-web }} storage-backend: ${{ steps.changes.outputs.storage-backend }} storage-web: ${{ steps.changes.outputs.storage-web }} @@ -96,7 +95,6 @@ jobs: echo "clock-web=true" >> $GITHUB_OUTPUT echo "contacts-backend=true" >> $GITHUB_OUTPUT echo "contacts-web=true" >> $GITHUB_OUTPUT - echo "presi-backend=true" >> $GITHUB_OUTPUT echo "presi-web=true" >> $GITHUB_OUTPUT echo "storage-backend=true" >> $GITHUB_OUTPUT echo "storage-web=true" >> $GITHUB_OUTPUT @@ -135,7 +133,6 @@ jobs: echo "clock-web=true" >> $GITHUB_OUTPUT echo "contacts-backend=true" >> $GITHUB_OUTPUT echo "contacts-web=true" >> $GITHUB_OUTPUT - echo "presi-backend=true" >> $GITHUB_OUTPUT echo "presi-web=true" >> $GITHUB_OUTPUT echo "storage-backend=true" >> $GITHUB_OUTPUT echo "storage-web=true" >> $GITHUB_OUTPUT @@ -275,13 +272,7 @@ jobs: echo "contacts-web=false" >> $GITHUB_OUTPUT fi - # presi-backend - PRESI_BACKEND_CHANGED=$(check_pattern "apps/presi/apps/backend/|apps/presi/packages/") - if [ "$COMMON_CHANGED" == "true" ] || [ "$SHARED_AUTH_CHANGED" == "true" ] || [ "$PRESI_BACKEND_CHANGED" == "true" ]; then - echo "presi-backend=true" >> $GITHUB_OUTPUT - else - echo "presi-backend=false" >> $GITHUB_OUTPUT - fi + # presi-backend: REMOVED — replaced by Hono server # presi-web PRESI_WEB_CHANGED=$(check_pattern "apps/presi/apps/web/|apps/presi/packages/") @@ -383,7 +374,7 @@ jobs: echo "| clock-web | ${{ steps.changes.outputs.clock-web }} |" >> $GITHUB_STEP_SUMMARY echo "| contacts-backend | ${{ steps.changes.outputs.contacts-backend }} |" >> $GITHUB_STEP_SUMMARY echo "| contacts-web | ${{ steps.changes.outputs.contacts-web }} |" >> $GITHUB_STEP_SUMMARY - echo "| presi-backend | ${{ steps.changes.outputs.presi-backend }} |" >> $GITHUB_STEP_SUMMARY + echo "| presi-backend | removed |" >> $GITHUB_STEP_SUMMARY echo "| presi-web | ${{ steps.changes.outputs.presi-web }} |" >> $GITHUB_STEP_SUMMARY echo "| storage-backend | ${{ steps.changes.outputs.storage-backend }} |" >> $GITHUB_STEP_SUMMARY echo "| storage-web | ${{ steps.changes.outputs.storage-web }} |" >> $GITHUB_STEP_SUMMARY @@ -808,34 +799,7 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - build-presi-backend: - name: Build presi-backend - runs-on: ubuntu-latest - needs: detect-changes - if: needs.detect-changes.outputs.presi-backend == 'true' - steps: - - uses: actions/checkout@v4 - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/metadata-action@v5 - id: meta - with: - images: ghcr.io/${{ github.repository_owner }}/presi-backend - tags: type=raw,value=latest - - uses: docker/build-push-action@v5 - with: - context: . - file: apps/presi/apps/backend/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - cache-from: type=gha - cache-to: type=gha,mode=max + # build-presi-backend: REMOVED — replaced by Hono server build-presi-web: name: Build presi-web diff --git a/apps/presi/apps/backend/Dockerfile b/apps/presi/apps/backend/Dockerfile deleted file mode 100644 index 4adbd938b..000000000 --- a/apps/presi/apps/backend/Dockerfile +++ /dev/null @@ -1,93 +0,0 @@ -# syntax=docker/dockerfile:1 -# Build stage -FROM node:20-alpine AS builder - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate - -WORKDIR /app - -# Copy root workspace files -COPY pnpm-workspace.yaml ./ -COPY package.json ./ -COPY pnpm-lock.yaml ./ -COPY patches ./patches - -# Copy shared packages (all required dependencies) -COPY packages/shared-drizzle-config ./packages/shared-drizzle-config -COPY packages/shared-errors ./packages/shared-errors -COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth -COPY packages/shared-nestjs-health ./packages/shared-nestjs-health -COPY packages/shared-nestjs-metrics ./packages/shared-nestjs-metrics -COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup -COPY packages/shared-tsconfig ./packages/shared-tsconfig -COPY packages/shared-error-tracking ./packages/shared-error-tracking -COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup - -# Copy presi packages and backend -COPY apps/presi/packages ./apps/presi/packages -COPY apps/presi/apps/backend ./apps/presi/apps/backend - -# Install dependencies (ignore scripts since generate-env.mjs isn't in Docker context) -RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --ignore-scripts - -# Build shared packages first (in dependency order) -WORKDIR /app/packages/shared-errors -RUN pnpm build - -WORKDIR /app/packages/shared-nestjs-auth -RUN pnpm build - -WORKDIR /app/packages/shared-nestjs-health -RUN pnpm build - -WORKDIR /app/packages/shared-nestjs-metrics -RUN pnpm build - -WORKDIR /app/packages/shared-nestjs-setup -RUN pnpm build - -# Build the backend - -WORKDIR /app/packages/shared-nestjs-setup -RUN pnpm build - -WORKDIR /app/packages/shared-error-tracking -RUN pnpm build - -WORKDIR /app/apps/presi/apps/backend -RUN pnpm build - -# Production stage -FROM node:20-alpine AS production - -# Install pnpm and postgresql-client for health checks -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \ - && apk add --no-cache postgresql-client - -WORKDIR /app - -# Copy everything from builder (including node_modules) -COPY --from=builder /app/pnpm-workspace.yaml ./ -COPY --from=builder /app/package.json ./ -COPY --from=builder /app/pnpm-lock.yaml ./ -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/packages ./packages -COPY --from=builder /app/apps/presi ./apps/presi - -# Copy entrypoint script -COPY apps/presi/apps/backend/docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -WORKDIR /app/apps/presi/apps/backend - -# Expose port -EXPOSE 3008 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3008/api/health || exit 1 - -# Run entrypoint script -ENTRYPOINT ["docker-entrypoint.sh"] -CMD ["node", "dist/main.js"] diff --git a/apps/presi/apps/backend/docker-entrypoint.sh b/apps/presi/apps/backend/docker-entrypoint.sh deleted file mode 100644 index dcc1356f9..000000000 --- a/apps/presi/apps/backend/docker-entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -e - -echo "Running database migrations..." -npx drizzle-kit push --config drizzle.config.ts --force || echo "Migration failed, continuing anyway..." - -# Start the application -echo "Starting Presi Backend..." -exec "$@" diff --git a/apps/presi/apps/backend/drizzle.config.ts b/apps/presi/apps/backend/drizzle.config.ts deleted file mode 100644 index dd5cde52c..000000000 --- a/apps/presi/apps/backend/drizzle.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -import 'dotenv/config'; -import { createDrizzleConfig } from '@manacore/shared-drizzle-config'; - -export default createDrizzleConfig({ dbName: 'presi' }); diff --git a/apps/presi/apps/backend/eslint.config.mjs b/apps/presi/apps/backend/eslint.config.mjs deleted file mode 100644 index 41ef245c0..000000000 --- a/apps/presi/apps/backend/eslint.config.mjs +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-check -import { - baseConfig, - typescriptConfig, - nestjsConfig, - prettierConfig, -} from '@manacore/eslint-config'; - -export default [ - { - ignores: ['dist/**', 'node_modules/**'], - }, - ...baseConfig, - ...typescriptConfig, - ...nestjsConfig, - ...prettierConfig, -]; diff --git a/apps/presi/apps/backend/jest.config.js b/apps/presi/apps/backend/jest.config.js deleted file mode 100644 index 2d7feb5b2..000000000 --- a/apps/presi/apps/backend/jest.config.js +++ /dev/null @@ -1,16 +0,0 @@ -/** @type {import('jest').Config} */ -module.exports = { - moduleFileExtensions: ['js', 'json', 'ts'], - rootDir: 'src', - testRegex: '.*\\.spec\\.ts$', - transform: { - '^.+\\.(t|j)s$': 'ts-jest', - }, - collectCoverageFrom: ['**/*.(t|j)s'], - coverageDirectory: '../coverage', - testEnvironment: 'node', - moduleNameMapper: { - '^@presi/shared$': '/../../packages/shared/src', - '^@manacore/shared-nestjs-auth$': '/../../../../../packages/shared-nestjs-auth/src', - }, -}; diff --git a/apps/presi/apps/backend/nest-cli.json b/apps/presi/apps/backend/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/apps/presi/apps/backend/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/apps/presi/apps/backend/package.json b/apps/presi/apps/backend/package.json deleted file mode 100644 index 4f93cfe0e..000000000 --- a/apps/presi/apps/backend/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@presi/backend", - "version": "0.2.0", - "private": true, - "scripts": { - "build": "nest build", - "start": "nest start", - "dev": "nest start --watch", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio", - "db:seed": "tsx src/db/seed.ts", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage" - }, - "dependencies": { - "@manacore/shared-error-tracking": "workspace:*", - "@manacore/shared-nestjs-auth": "workspace:*", - "@manacore/shared-nestjs-health": "workspace:*", - "@manacore/shared-nestjs-metrics": "workspace:*", - "@manacore/shared-nestjs-setup": "workspace:*", - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "@nestjs/swagger": "^11.2.6", - "@presi/shared": "workspace:*", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", - "dotenv": "^16.4.7", - "drizzle-kit": "^0.30.2", - "@nestjs/throttler": "^6.2.1", - "drizzle-orm": "^0.38.3", - "postgres": "^3.4.5", - "prom-client": "^15.1.0", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "nanoid": "^5.0.9" - }, - "devDependencies": { - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@nestjs/testing": "^11.1.17", - "@types/express": "^5.0.0", - "@types/jest": "^30.0.0", - "@types/node": "^22.10.2", - "jest": "^30.3.0", - "ts-jest": "^29.2.5", - "tsx": "^4.19.2", - "typescript": "^5.7.2" - } -} diff --git a/apps/presi/apps/backend/src/admin/__tests__/admin.controller.spec.ts b/apps/presi/apps/backend/src/admin/__tests__/admin.controller.spec.ts deleted file mode 100644 index 758b11396..000000000 --- a/apps/presi/apps/backend/src/admin/__tests__/admin.controller.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { AdminController } from '../admin.controller'; - -describe('AdminController', () => { - let controller: AdminController; - let service: any; - - beforeEach(() => { - service = { - getUserData: jest.fn(), - deleteUserData: jest.fn(), - }; - controller = new AdminController(service); - }); - - afterEach(() => jest.clearAllMocks()); - - describe('getUserData', () => { - it('should return user data summary', async () => { - const userData = { - entities: [ - { entity: 'decks', count: 5, label: 'Decks' }, - { entity: 'slides', count: 20, label: 'Slides' }, - { entity: 'shared_decks', count: 3, label: 'Shared Links' }, - ], - totalCount: 28, - lastActivityAt: '2025-06-01T00:00:00.000Z', - }; - service.getUserData.mockResolvedValue(userData); - - const result = await controller.getUserData('user-1'); - - expect(result).toEqual(userData); - expect(service.getUserData).toHaveBeenCalledWith('user-1'); - }); - - it('should return empty data for unknown user', async () => { - const emptyData = { entities: [], totalCount: 0, lastActivityAt: undefined }; - service.getUserData.mockResolvedValue(emptyData); - - const result = await controller.getUserData('unknown'); - - expect(result.totalCount).toBe(0); - }); - }); - - describe('deleteUserData', () => { - it('should delete all user data and return counts', async () => { - const deleteResult = { - success: true, - deletedCounts: [ - { entity: 'shared_decks', count: 2, label: 'Shared Links' }, - { entity: 'slides', count: 10, label: 'Slides' }, - { entity: 'decks', count: 3, label: 'Decks' }, - ], - totalDeleted: 15, - }; - service.deleteUserData.mockResolvedValue(deleteResult); - - const result = await controller.deleteUserData('user-1'); - - expect(result.success).toBe(true); - expect(result.totalDeleted).toBe(15); - expect(service.deleteUserData).toHaveBeenCalledWith('user-1'); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/admin/__tests__/admin.service.spec.ts b/apps/presi/apps/backend/src/admin/__tests__/admin.service.spec.ts deleted file mode 100644 index d1a88e41a..000000000 --- a/apps/presi/apps/backend/src/admin/__tests__/admin.service.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AdminService } from '../admin.service'; -import { DATABASE_CONNECTION } from '../../db/database.module'; - -const TEST_USER_ID = 'user-1'; - -describe('AdminService', () => { - let service: AdminService; - let mockDb: any; - - beforeEach(async () => { - mockDb = { - select: jest.fn().mockReturnThis(), - from: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - orderBy: jest.fn().mockReturnThis(), - limit: jest.fn(), - delete: jest.fn().mockReturnThis(), - returning: jest.fn(), - }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - AdminService, - { - provide: DATABASE_CONNECTION, - useValue: mockDb, - }, - ], - }).compile(); - - service = module.get(AdminService); - }); - - describe('getUserData', () => { - it('should return user data summary with counts', async () => { - // Count decks - mockDb.where.mockResolvedValueOnce([{ count: 3 }]); - // Get user decks - mockDb.where.mockResolvedValueOnce([{ id: 'deck-1' }, { id: 'deck-2' }, { id: 'deck-3' }]); - // Count slides - mockDb.where.mockResolvedValueOnce([{ count: 10 }]); - // Count shared decks - mockDb.where.mockResolvedValueOnce([{ count: 2 }]); - // Last activity - mockDb.limit.mockResolvedValue([{ updatedAt: new Date('2025-06-01') }]); - - const result = await service.getUserData(TEST_USER_ID); - - expect(result.totalCount).toBe(15); - expect(result.entities).toHaveLength(3); - expect(result.entities[0]).toEqual({ entity: 'decks', count: 3, label: 'Decks' }); - expect(result.entities[1]).toEqual({ entity: 'slides', count: 10, label: 'Slides' }); - expect(result.entities[2]).toEqual({ - entity: 'shared_decks', - count: 2, - label: 'Shared Links', - }); - expect(result.lastActivityAt).toBe('2025-06-01T00:00:00.000Z'); - }); - - it('should return zeros when user has no data', async () => { - // Count decks - mockDb.where.mockResolvedValueOnce([{ count: 0 }]); - // Get user decks (empty) - mockDb.where.mockResolvedValueOnce([]); - // Last activity (none) - mockDb.limit.mockResolvedValue([]); - - const result = await service.getUserData(TEST_USER_ID); - - expect(result.totalCount).toBe(0); - expect(result.lastActivityAt).toBeUndefined(); - }); - }); - - describe('deleteUserData', () => { - it('should delete all user data and return counts', async () => { - // Get user decks - mockDb.where.mockResolvedValueOnce([{ id: 'deck-1' }]); - // Delete shared decks - mockDb.returning.mockResolvedValueOnce([{ id: 'share-1' }]); - // Delete slides - mockDb.returning.mockResolvedValueOnce([{ id: 'slide-1' }, { id: 'slide-2' }]); - // Delete decks - mockDb.returning.mockResolvedValueOnce([{ id: 'deck-1' }]); - - const result = await service.deleteUserData(TEST_USER_ID); - - expect(result.success).toBe(true); - expect(result.totalDeleted).toBe(4); - expect(result.deletedCounts).toHaveLength(3); - }); - - it('should handle user with no data gracefully', async () => { - // Get user decks (empty) - mockDb.where.mockResolvedValueOnce([]); - // Delete decks (none) - mockDb.returning.mockResolvedValueOnce([]); - - const result = await service.deleteUserData(TEST_USER_ID); - - expect(result.success).toBe(true); - expect(result.totalDeleted).toBe(0); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/admin/admin.controller.ts b/apps/presi/apps/backend/src/admin/admin.controller.ts deleted file mode 100644 index 911acfc1f..000000000 --- a/apps/presi/apps/backend/src/admin/admin.controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - Controller, - Get, - Delete, - Param, - UseGuards, - Logger, - HttpCode, - HttpStatus, -} from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { AdminService } from './admin.service'; -import { ServiceAuthGuard } from './guards/service-auth.guard'; -import { UserDataResponse, DeleteUserDataResponse } from './dto/user-data-response.dto'; - -@ApiTags('Admin') -@Controller('admin') -@UseGuards(ServiceAuthGuard) -export class AdminController { - private readonly logger = new Logger(AdminController.name); - - constructor(private readonly adminService: AdminService) {} - - /** - * Get user data summary for this backend - * Called by mana-core-auth admin service to aggregate cross-project data - */ - @Get('user-data/:userId') - async getUserData(@Param('userId') userId: string): Promise { - this.logger.log(`Admin request: getUserData for userId=${userId}`); - return this.adminService.getUserData(userId); - } - - /** - * Delete all user data from this backend (GDPR right to be forgotten) - * Called by mana-core-auth admin service during cross-project deletion - */ - @Delete('user-data/:userId') - @HttpCode(HttpStatus.OK) - async deleteUserData(@Param('userId') userId: string): Promise { - this.logger.log(`Admin request: deleteUserData for userId=${userId}`); - return this.adminService.deleteUserData(userId); - } -} diff --git a/apps/presi/apps/backend/src/admin/admin.module.ts b/apps/presi/apps/backend/src/admin/admin.module.ts deleted file mode 100644 index 5ceacac37..000000000 --- a/apps/presi/apps/backend/src/admin/admin.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { AdminController } from './admin.controller'; -import { AdminService } from './admin.service'; -import { ServiceAuthGuard } from './guards/service-auth.guard'; - -@Module({ - imports: [ConfigModule], - controllers: [AdminController], - providers: [AdminService, ServiceAuthGuard], -}) -export class AdminModule {} diff --git a/apps/presi/apps/backend/src/admin/admin.service.ts b/apps/presi/apps/backend/src/admin/admin.service.ts deleted file mode 100644 index 9f10c7d54..000000000 --- a/apps/presi/apps/backend/src/admin/admin.service.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Injectable, Logger, Inject } from '@nestjs/common'; -import { eq, sql, desc, inArray } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import type { Database } from '../db/connection'; -import * as schema from '../db/schema'; -import { - UserDataResponse, - DeleteUserDataResponse, - EntityCount, -} from './dto/user-data-response.dto'; - -@Injectable() -export class AdminService { - private readonly logger = new Logger(AdminService.name); - - constructor( - @Inject(DATABASE_CONNECTION) - private readonly db: Database - ) {} - - async getUserData(userId: string): Promise { - this.logger.log(`Getting user data for userId: ${userId}`); - - // Count decks - const decksResult = await this.db - .select({ count: sql`count(*)::int` }) - .from(schema.decks) - .where(eq(schema.decks.userId, userId)); - const decksCount = decksResult[0]?.count ?? 0; - - // Count slides (through decks) - const userDecks = await this.db - .select({ id: schema.decks.id }) - .from(schema.decks) - .where(eq(schema.decks.userId, userId)); - - let slidesCount = 0; - if (userDecks.length > 0) { - const deckIds = userDecks.map((d) => d.id); - const slidesResult = await this.db - .select({ count: sql`count(*)::int` }) - .from(schema.slides) - .where(inArray(schema.slides.deckId, deckIds)); - slidesCount = slidesResult[0]?.count ?? 0; - } - - // Count shared decks (through decks) - let sharedDecksCount = 0; - if (userDecks.length > 0) { - const deckIds = userDecks.map((d) => d.id); - const sharedResult = await this.db - .select({ count: sql`count(*)::int` }) - .from(schema.sharedDecks) - .where(inArray(schema.sharedDecks.deckId, deckIds)); - sharedDecksCount = sharedResult[0]?.count ?? 0; - } - - // Get last activity - const lastDeck = await this.db - .select({ updatedAt: schema.decks.updatedAt }) - .from(schema.decks) - .where(eq(schema.decks.userId, userId)) - .orderBy(desc(schema.decks.updatedAt)) - .limit(1); - const lastActivityAt = lastDeck[0]?.updatedAt?.toISOString(); - - const entities: EntityCount[] = [ - { entity: 'decks', count: decksCount, label: 'Decks' }, - { entity: 'slides', count: slidesCount, label: 'Slides' }, - { entity: 'shared_decks', count: sharedDecksCount, label: 'Shared Links' }, - ]; - - const totalCount = decksCount + slidesCount + sharedDecksCount; - - return { entities, totalCount, lastActivityAt }; - } - - async deleteUserData(userId: string): Promise { - this.logger.log(`Deleting user data for userId: ${userId}`); - - const deletedCounts: EntityCount[] = []; - let totalDeleted = 0; - - // Get user's decks first - const userDecks = await this.db - .select({ id: schema.decks.id }) - .from(schema.decks) - .where(eq(schema.decks.userId, userId)); - - if (userDecks.length > 0) { - const deckIds = userDecks.map((d) => d.id); - - // Delete shared decks - const deletedShared = await this.db - .delete(schema.sharedDecks) - .where(inArray(schema.sharedDecks.deckId, deckIds)) - .returning(); - deletedCounts.push({ - entity: 'shared_decks', - count: deletedShared.length, - label: 'Shared Links', - }); - totalDeleted += deletedShared.length; - - // Delete slides (cascade should handle this, but let's be explicit) - const deletedSlides = await this.db - .delete(schema.slides) - .where(inArray(schema.slides.deckId, deckIds)) - .returning(); - deletedCounts.push({ entity: 'slides', count: deletedSlides.length, label: 'Slides' }); - totalDeleted += deletedSlides.length; - } - - // Delete decks - const deletedDecks = await this.db - .delete(schema.decks) - .where(eq(schema.decks.userId, userId)) - .returning(); - deletedCounts.push({ entity: 'decks', count: deletedDecks.length, label: 'Decks' }); - totalDeleted += deletedDecks.length; - - this.logger.log(`Deleted ${totalDeleted} records for userId: ${userId}`); - - return { success: true, deletedCounts, totalDeleted }; - } -} diff --git a/apps/presi/apps/backend/src/admin/dto/user-data-response.dto.ts b/apps/presi/apps/backend/src/admin/dto/user-data-response.dto.ts deleted file mode 100644 index 562a2eb6d..000000000 --- a/apps/presi/apps/backend/src/admin/dto/user-data-response.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface EntityCount { - entity: string; - count: number; - label: string; -} - -export interface UserDataResponse { - entities: EntityCount[]; - totalCount: number; - lastActivityAt?: string; -} - -export interface DeleteUserDataResponse { - success: boolean; - deletedCounts: EntityCount[]; - totalDeleted: number; -} diff --git a/apps/presi/apps/backend/src/admin/guards/service-auth.guard.ts b/apps/presi/apps/backend/src/admin/guards/service-auth.guard.ts deleted file mode 100644 index 81b60d0a4..000000000 --- a/apps/presi/apps/backend/src/admin/guards/service-auth.guard.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - Injectable, - CanActivate, - ExecutionContext, - UnauthorizedException, - Logger, -} from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { Request } from 'express'; - -/** - * Guard for internal service-to-service authentication using X-Service-Key header - * Used by mana-core-auth to query user data across backends - */ -@Injectable() -export class ServiceAuthGuard implements CanActivate { - private readonly logger = new Logger(ServiceAuthGuard.name); - private readonly serviceKey: string; - - constructor(private readonly configService: ConfigService) { - this.serviceKey = this.configService.get('ADMIN_SERVICE_KEY', 'dev-admin-key'); - } - - canActivate(context: ExecutionContext): boolean { - const request = context.switchToHttp().getRequest(); - const providedKey = request.headers['x-service-key'] as string; - - if (!providedKey) { - this.logger.warn('Missing X-Service-Key header'); - throw new UnauthorizedException('Missing service key'); - } - - if (providedKey !== this.serviceKey) { - this.logger.warn('Invalid service key provided'); - throw new UnauthorizedException('Invalid service key'); - } - - return true; - } -} diff --git a/apps/presi/apps/backend/src/app.module.ts b/apps/presi/apps/backend/src/app.module.ts deleted file mode 100644 index 83786ad64..000000000 --- a/apps/presi/apps/backend/src/app.module.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Module } from '@nestjs/common'; -import { APP_GUARD } from '@nestjs/core'; -import { ConfigModule } from '@nestjs/config'; -import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; -import { DatabaseModule } from './db/database.module'; -import { DeckModule } from './deck/deck.module'; -import { SlideModule } from './slide/slide.module'; -import { ThemeModule } from './theme/theme.module'; -import { ShareModule } from './share/share.module'; -import { AdminModule } from './admin/admin.module'; -import { HealthModule } from '@manacore/shared-nestjs-health'; -import { MetricsModule } from '@manacore/shared-nestjs-metrics'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: '.env', - }), - ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]), - DatabaseModule, - DeckModule, - SlideModule, - ThemeModule, - ShareModule, - AdminModule, - HealthModule.forRoot({ serviceName: 'presi-backend' }), - MetricsModule.register({ - prefix: 'presi_', - excludePaths: ['/health'], - }), - ], - providers: [ - { - provide: APP_GUARD, - useClass: ThrottlerGuard, - }, - ], -}) -export class AppModule {} diff --git a/apps/presi/apps/backend/src/db/connection.ts b/apps/presi/apps/backend/src/db/connection.ts deleted file mode 100644 index 5c4dbb304..000000000 --- a/apps/presi/apps/backend/src/db/connection.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { drizzle } from 'drizzle-orm/postgres-js'; -import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import * as schema from './schema'; - -// Use require for postgres to avoid ESM/CommonJS interop issues - -const postgres = require('postgres'); - -let connection: ReturnType | null = null; -let db: PostgresJsDatabase | null = null; - -export function getConnection(databaseUrl: string) { - if (!connection) { - connection = postgres(databaseUrl, { - max: 10, - idle_timeout: 20, - connect_timeout: 10, - }); - } - return connection; -} - -export function getDb(databaseUrl: string): PostgresJsDatabase { - if (!db) { - const conn = getConnection(databaseUrl); - db = drizzle(conn, { schema }); - } - return db; -} - -export async function closeConnection() { - if (connection) { - await connection.end(); - connection = null; - db = null; - } -} - -export type Database = PostgresJsDatabase; diff --git a/apps/presi/apps/backend/src/db/database.module.ts b/apps/presi/apps/backend/src/db/database.module.ts deleted file mode 100644 index b4d1f2af6..000000000 --- a/apps/presi/apps/backend/src/db/database.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Module, Global, OnModuleDestroy } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { getDb, closeConnection, type Database } from './connection'; - -export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_CONNECTION, - useFactory: (configService: ConfigService): Database => { - const databaseUrl = configService.get('DATABASE_URL'); - if (!databaseUrl) { - throw new Error('DATABASE_URL environment variable is not set'); - } - return getDb(databaseUrl); - }, - inject: [ConfigService], - }, - ], - exports: [DATABASE_CONNECTION], -}) -export class DatabaseModule implements OnModuleDestroy { - async onModuleDestroy() { - await closeConnection(); - } -} diff --git a/apps/presi/apps/backend/src/db/migrations/0001_user_id_to_text.sql b/apps/presi/apps/backend/src/db/migrations/0001_user_id_to_text.sql deleted file mode 100644 index ad49f9045..000000000 --- a/apps/presi/apps/backend/src/db/migrations/0001_user_id_to_text.sql +++ /dev/null @@ -1,20 +0,0 @@ --- Migration: Change user_id from UUID to TEXT --- This allows compatibility with Better Auth nanoid-based user IDs - --- Step 1: Add a temporary column with the new type -ALTER TABLE decks ADD COLUMN user_id_new TEXT; - --- Step 2: Copy and convert existing data (UUID to TEXT) -UPDATE decks SET user_id_new = user_id::text WHERE user_id IS NOT NULL; - --- Step 3: Drop the old column -ALTER TABLE decks DROP COLUMN user_id; - --- Step 4: Rename the new column -ALTER TABLE decks RENAME COLUMN user_id_new TO user_id; - --- Step 5: Add NOT NULL constraint -ALTER TABLE decks ALTER COLUMN user_id SET NOT NULL; - --- Step 6: Add index for performance -CREATE INDEX IF NOT EXISTS decks_user_id_idx ON decks(user_id); diff --git a/apps/presi/apps/backend/src/db/schema/decks.schema.ts b/apps/presi/apps/backend/src/db/schema/decks.schema.ts deleted file mode 100644 index a2bb677bb..000000000 --- a/apps/presi/apps/backend/src/db/schema/decks.schema.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { pgTable, uuid, text, boolean, timestamp, index } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { slides } from './slides.schema'; -import { themes } from './themes.schema'; -import { sharedDecks } from './shared-decks.schema'; - -export const decks = pgTable( - 'decks', - { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), // TEXT for Better Auth nanoid user IDs - title: text('title').notNull(), - description: text('description'), - themeId: uuid('theme_id').references(() => themes.id), - isPublic: boolean('is_public').default(false).notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('decks_user_id_idx').on(table.userId), - index('decks_user_updated_idx').on(table.userId, table.updatedAt), - index('decks_theme_id_idx').on(table.themeId), - ] -); - -export const decksRelations = relations(decks, ({ many, one }) => ({ - slides: many(slides), - theme: one(themes, { - fields: [decks.themeId], - references: [themes.id], - }), - sharedDecks: many(sharedDecks), -})); diff --git a/apps/presi/apps/backend/src/db/schema/index.ts b/apps/presi/apps/backend/src/db/schema/index.ts deleted file mode 100644 index eff7c7b78..000000000 --- a/apps/presi/apps/backend/src/db/schema/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './decks.schema'; -export * from './slides.schema'; -export * from './themes.schema'; -export * from './shared-decks.schema'; diff --git a/apps/presi/apps/backend/src/db/schema/shared-decks.schema.ts b/apps/presi/apps/backend/src/db/schema/shared-decks.schema.ts deleted file mode 100644 index 805f6bbb4..000000000 --- a/apps/presi/apps/backend/src/db/schema/shared-decks.schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { pgTable, uuid, text, timestamp, index } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { decks } from './decks.schema'; - -export const sharedDecks = pgTable( - 'shared_decks', - { - id: uuid('id').primaryKey().defaultRandom(), - deckId: uuid('deck_id') - .notNull() - .references(() => decks.id, { onDelete: 'cascade' }), - shareCode: text('share_code').notNull().unique(), - expiresAt: timestamp('expires_at', { withTimezone: true }), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [index('shared_decks_deck_id_idx').on(table.deckId)] -); - -export const sharedDecksRelations = relations(sharedDecks, ({ one }) => ({ - deck: one(decks, { - fields: [sharedDecks.deckId], - references: [decks.id], - }), -})); diff --git a/apps/presi/apps/backend/src/db/schema/slides.schema.ts b/apps/presi/apps/backend/src/db/schema/slides.schema.ts deleted file mode 100644 index c66f0f579..000000000 --- a/apps/presi/apps/backend/src/db/schema/slides.schema.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { pgTable, uuid, integer, jsonb, timestamp, index } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { decks } from './decks.schema'; - -export const slides = pgTable( - 'slides', - { - id: uuid('id').primaryKey().defaultRandom(), - deckId: uuid('deck_id') - .notNull() - .references(() => decks.id, { onDelete: 'cascade' }), - order: integer('order').notNull(), - content: jsonb('content').$type<{ - type: 'title' | 'content' | 'image' | 'split'; - title?: string; - subtitle?: string; - body?: string; - imageUrl?: string; - bulletPoints?: string[]; - }>(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - }, - (table) => [ - index('slides_deck_id_idx').on(table.deckId), - index('slides_deck_order_idx').on(table.deckId, table.order), - ] -); - -export const slidesRelations = relations(slides, ({ one }) => ({ - deck: one(decks, { - fields: [slides.deckId], - references: [decks.id], - }), -})); diff --git a/apps/presi/apps/backend/src/db/schema/themes.schema.ts b/apps/presi/apps/backend/src/db/schema/themes.schema.ts deleted file mode 100644 index eac842a28..000000000 --- a/apps/presi/apps/backend/src/db/schema/themes.schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { pgTable, uuid, text, jsonb, boolean } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; -import { decks } from './decks.schema'; - -export const themes = pgTable('themes', { - id: uuid('id').primaryKey().defaultRandom(), - name: text('name').notNull(), - colors: jsonb('colors').$type<{ - primary: string; - secondary: string; - background: string; - text: string; - accent: string; - }>(), - fonts: jsonb('fonts').$type<{ - heading: string; - body: string; - }>(), - isDefault: boolean('is_default').default(false).notNull(), -}); - -export const themesRelations = relations(themes, ({ many }) => ({ - decks: many(decks), -})); diff --git a/apps/presi/apps/backend/src/deck/__tests__/deck.controller.spec.ts b/apps/presi/apps/backend/src/deck/__tests__/deck.controller.spec.ts deleted file mode 100644 index 9f3ae15a3..000000000 --- a/apps/presi/apps/backend/src/deck/__tests__/deck.controller.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { DeckController } from '../deck.controller'; - -const TEST_USER_ID = 'test-user-123'; -const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' }; - -function createMockDeck(overrides: Record = {}) { - return { - id: 'deck-1', - userId: TEST_USER_ID, - title: 'Test Deck', - description: 'A test deck', - themeId: null, - isPublic: false, - createdAt: new Date(), - updatedAt: new Date(), - ...overrides, - }; -} - -describe('DeckController', () => { - let controller: DeckController; - let service: any; - - beforeEach(() => { - service = { - findByUser: jest.fn(), - findOneWithSlides: jest.fn(), - create: jest.fn(), - update: jest.fn(), - remove: jest.fn(), - }; - controller = new DeckController(service); - }); - - afterEach(() => jest.clearAllMocks()); - - describe('findAll', () => { - it('should return user decks', async () => { - const decks = [createMockDeck()]; - service.findByUser.mockResolvedValue(decks); - - const result = await controller.findAll(mockUser as any); - - expect(result).toEqual(decks); - expect(service.findByUser).toHaveBeenCalledWith(TEST_USER_ID); - }); - }); - - describe('findOne', () => { - it('should return deck with slides', async () => { - const deck = createMockDeck({ slides: [] }); - service.findOneWithSlides.mockResolvedValue(deck); - - const result = await controller.findOne('deck-1', mockUser as any); - - expect(result).toEqual(deck); - expect(service.findOneWithSlides).toHaveBeenCalledWith('deck-1', TEST_USER_ID); - }); - }); - - describe('create', () => { - it('should create and return deck', async () => { - const deck = createMockDeck(); - service.create.mockResolvedValue(deck); - - const result = await controller.create({ title: 'Test Deck' } as any, mockUser as any); - - expect(result).toEqual(deck); - expect(service.create).toHaveBeenCalledWith(TEST_USER_ID, { title: 'Test Deck' }); - }); - }); - - describe('update', () => { - it('should update and return deck', async () => { - const deck = createMockDeck({ title: 'Updated' }); - service.update.mockResolvedValue(deck); - - const result = await controller.update( - 'deck-1', - { title: 'Updated' } as any, - mockUser as any - ); - - expect(result).toEqual(deck); - expect(service.update).toHaveBeenCalledWith('deck-1', TEST_USER_ID, { title: 'Updated' }); - }); - }); - - describe('remove', () => { - it('should delete and return success', async () => { - service.remove.mockResolvedValue({ success: true }); - - const result = await controller.remove('deck-1', mockUser as any); - - expect(result).toEqual({ success: true }); - expect(service.remove).toHaveBeenCalledWith('deck-1', TEST_USER_ID); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/deck/__tests__/deck.service.spec.ts b/apps/presi/apps/backend/src/deck/__tests__/deck.service.spec.ts deleted file mode 100644 index e7c13f67a..000000000 --- a/apps/presi/apps/backend/src/deck/__tests__/deck.service.spec.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { NotFoundException } from '@nestjs/common'; -import { DeckService } from '../deck.service'; -import { DATABASE_CONNECTION } from '../../db/database.module'; - -const TEST_USER_ID = 'user-1'; -const TEST_DECK_ID = 'deck-1'; - -function createMockDeck(overrides: Record = {}) { - return { - id: TEST_DECK_ID, - userId: TEST_USER_ID, - title: 'Test Deck', - description: 'A test deck', - themeId: null, - isPublic: false, - createdAt: new Date('2025-01-01'), - updatedAt: new Date('2025-01-01'), - theme: null, - slides: [], - ...overrides, - }; -} - -describe('DeckService', () => { - let service: DeckService; - let mockDb: any; - - beforeEach(async () => { - mockDb = { - query: { - decks: { - findMany: jest.fn(), - findFirst: jest.fn(), - }, - }, - insert: jest.fn().mockReturnThis(), - values: jest.fn().mockReturnThis(), - returning: jest.fn(), - update: jest.fn().mockReturnThis(), - set: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - delete: jest.fn().mockReturnThis(), - }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - DeckService, - { - provide: DATABASE_CONNECTION, - useValue: mockDb, - }, - ], - }).compile(); - - service = module.get(DeckService); - }); - - describe('findByUser', () => { - it('should return all decks for a user', async () => { - const decks = [createMockDeck(), createMockDeck({ id: 'deck-2', title: 'Second Deck' })]; - mockDb.query.decks.findMany.mockResolvedValue(decks); - - const result = await service.findByUser(TEST_USER_ID); - - expect(result).toEqual(decks); - expect(mockDb.query.decks.findMany).toHaveBeenCalledWith( - expect.objectContaining({ - with: { theme: true }, - }) - ); - }); - - it('should return empty array when user has no decks', async () => { - mockDb.query.decks.findMany.mockResolvedValue([]); - - const result = await service.findByUser(TEST_USER_ID); - - expect(result).toEqual([]); - }); - }); - - describe('findOneWithSlides', () => { - it('should return deck with slides when found', async () => { - const deck = createMockDeck({ - slides: [{ id: 'slide-1', order: 0, content: { type: 'title' } }], - }); - mockDb.query.decks.findFirst.mockResolvedValue(deck); - - const result = await service.findOneWithSlides(TEST_DECK_ID, TEST_USER_ID); - - expect(result).toEqual(deck); - expect(result.slides).toHaveLength(1); - }); - - it('should throw NotFoundException when deck not found', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(null); - - await expect(service.findOneWithSlides('nonexistent', TEST_USER_ID)).rejects.toThrow( - NotFoundException - ); - }); - - it('should throw NotFoundException when user does not own deck', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(null); - - await expect(service.findOneWithSlides(TEST_DECK_ID, 'other-user')).rejects.toThrow( - NotFoundException - ); - }); - }); - - describe('findOne', () => { - it('should return deck without ownership check', async () => { - const deck = createMockDeck(); - mockDb.query.decks.findFirst.mockResolvedValue(deck); - - const result = await service.findOne(TEST_DECK_ID); - - expect(result).toEqual(deck); - }); - - it('should return undefined when deck not found', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(undefined); - - const result = await service.findOne('nonexistent'); - - expect(result).toBeUndefined(); - }); - }); - - describe('create', () => { - it('should create and return a new deck', async () => { - const newDeck = createMockDeck(); - mockDb.returning.mockResolvedValue([newDeck]); - - const result = await service.create(TEST_USER_ID, { - title: 'Test Deck', - description: 'A test deck', - }); - - expect(result).toEqual(newDeck); - expect(mockDb.insert).toHaveBeenCalled(); - expect(mockDb.values).toHaveBeenCalledWith( - expect.objectContaining({ - userId: TEST_USER_ID, - title: 'Test Deck', - description: 'A test deck', - }) - ); - }); - - it('should create deck with themeId', async () => { - const deck = createMockDeck({ themeId: 'theme-1' }); - mockDb.returning.mockResolvedValue([deck]); - - const result = await service.create(TEST_USER_ID, { - title: 'Themed Deck', - themeId: 'theme-1', - }); - - expect(result.themeId).toBe('theme-1'); - }); - }); - - describe('update', () => { - it('should update and return the deck', async () => { - const existing = createMockDeck(); - const updated = createMockDeck({ title: 'Updated Title' }); - mockDb.query.decks.findFirst.mockResolvedValue(existing); - mockDb.returning.mockResolvedValue([updated]); - - const result = await service.update(TEST_DECK_ID, TEST_USER_ID, { title: 'Updated Title' }); - - expect(result).toEqual(updated); - expect(mockDb.update).toHaveBeenCalled(); - }); - - it('should throw NotFoundException when deck not found', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(null); - - await expect( - service.update('nonexistent', TEST_USER_ID, { title: 'Updated' }) - ).rejects.toThrow(NotFoundException); - }); - - it('should throw NotFoundException when user does not own deck', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(null); - - await expect( - service.update(TEST_DECK_ID, 'other-user', { title: 'Updated' }) - ).rejects.toThrow(NotFoundException); - }); - }); - - describe('remove', () => { - it('should delete deck and return success', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(createMockDeck()); - mockDb.where.mockResolvedValue(undefined); - - const result = await service.remove(TEST_DECK_ID, TEST_USER_ID); - - expect(result).toEqual({ success: true }); - expect(mockDb.delete).toHaveBeenCalled(); - }); - - it('should throw NotFoundException when deck not found', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(null); - - await expect(service.remove('nonexistent', TEST_USER_ID)).rejects.toThrow(NotFoundException); - }); - }); - - describe('verifyOwnership', () => { - it('should return true when user owns deck', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(createMockDeck()); - - const result = await service.verifyOwnership(TEST_DECK_ID, TEST_USER_ID); - - expect(result).toBe(true); - }); - - it('should return false when user does not own deck', async () => { - mockDb.query.decks.findFirst.mockResolvedValue(null); - - const result = await service.verifyOwnership(TEST_DECK_ID, 'other-user'); - - expect(result).toBe(false); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/deck/deck.controller.ts b/apps/presi/apps/backend/src/deck/deck.controller.ts deleted file mode 100644 index f44248d25..000000000 --- a/apps/presi/apps/backend/src/deck/deck.controller.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - UseGuards, - ParseUUIDPipe, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { DeckService } from './deck.service'; -import { CreateDeckDto } from './deck.dto'; -import type { UpdateDeckDto } from './deck.dto'; -import { JwtAuthGuard, CurrentUser } from '@manacore/shared-nestjs-auth'; -import type { CurrentUserData } from '@manacore/shared-nestjs-auth'; - -@ApiTags('Decks') -@ApiBearerAuth() -@Controller('decks') -@UseGuards(JwtAuthGuard) -export class DeckController { - constructor(private readonly deckService: DeckService) {} - - @Get() - async findAll(@CurrentUser() user: CurrentUserData) { - return this.deckService.findByUser(user.userId); - } - - @Get(':id') - async findOne(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: CurrentUserData) { - return this.deckService.findOneWithSlides(id, user.userId); - } - - @Post() - async create(@Body() createDeckDto: CreateDeckDto, @CurrentUser() user: CurrentUserData) { - return this.deckService.create(user.userId, createDeckDto); - } - - @Put(':id') - async update( - @Param('id', ParseUUIDPipe) id: string, - @Body() updateDeckDto: UpdateDeckDto, - @CurrentUser() user: CurrentUserData - ) { - return this.deckService.update(id, user.userId, updateDeckDto); - } - - @Delete(':id') - async remove(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: CurrentUserData) { - return this.deckService.remove(id, user.userId); - } -} diff --git a/apps/presi/apps/backend/src/deck/deck.dto.ts b/apps/presi/apps/backend/src/deck/deck.dto.ts deleted file mode 100644 index c182015ca..000000000 --- a/apps/presi/apps/backend/src/deck/deck.dto.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - IsString, - IsOptional, - IsBoolean, - IsUUID, - MinLength, - MaxLength, - IsNotEmpty, -} from 'class-validator'; - -export class CreateDeckDto { - @IsString() - @IsNotEmpty() - @MinLength(1) - @MaxLength(200) - title: string; - - @IsString() - @IsOptional() - @MaxLength(2000) - description?: string; - - @IsUUID() - @IsOptional() - themeId?: string; -} - -export class UpdateDeckDto { - @IsString() - @IsOptional() - @MinLength(1) - @MaxLength(200) - title?: string; - - @IsString() - @IsOptional() - @MaxLength(2000) - description?: string; - - @IsUUID() - @IsOptional() - themeId?: string; - - @IsBoolean() - @IsOptional() - isPublic?: boolean; -} diff --git a/apps/presi/apps/backend/src/deck/deck.module.ts b/apps/presi/apps/backend/src/deck/deck.module.ts deleted file mode 100644 index 873e5abb6..000000000 --- a/apps/presi/apps/backend/src/deck/deck.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { DeckController } from './deck.controller'; -import { DeckService } from './deck.service'; - -@Module({ - controllers: [DeckController], - providers: [DeckService], - exports: [DeckService], -}) -export class DeckModule {} diff --git a/apps/presi/apps/backend/src/deck/deck.service.ts b/apps/presi/apps/backend/src/deck/deck.service.ts deleted file mode 100644 index d09e7c10a..000000000 --- a/apps/presi/apps/backend/src/deck/deck.service.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { eq, and, desc } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import { Database } from '../db/connection'; -import { decks, slides } from '../db/schema'; -import { CreateDeckDto } from './deck.dto'; -import type { UpdateDeckDto } from './deck.dto'; - -@Injectable() -export class DeckService { - constructor( - @Inject(DATABASE_CONNECTION) - private readonly db: Database - ) {} - - async findByUser(userId: string) { - return this.db.query.decks.findMany({ - where: eq(decks.userId, userId), - orderBy: [desc(decks.updatedAt)], - with: { - theme: true, - }, - }); - } - - async findOneWithSlides(id: string, userId: string) { - const deck = await this.db.query.decks.findFirst({ - where: and(eq(decks.id, id), eq(decks.userId, userId)), - with: { - slides: { - orderBy: [slides.order], - }, - theme: true, - }, - }); - - if (!deck) { - throw new NotFoundException('Deck not found'); - } - - return deck; - } - - async findOne(id: string) { - return this.db.query.decks.findFirst({ - where: eq(decks.id, id), - with: { - slides: { - orderBy: [slides.order], - }, - theme: true, - }, - }); - } - - async create(userId: string, dto: CreateDeckDto) { - const [deck] = await this.db - .insert(decks) - .values({ - userId, - title: dto.title, - description: dto.description, - themeId: dto.themeId, - }) - .returning(); - - return deck; - } - - async update(id: string, userId: string, dto: UpdateDeckDto) { - // Verify ownership - const existing = await this.db.query.decks.findFirst({ - where: and(eq(decks.id, id), eq(decks.userId, userId)), - }); - - if (!existing) { - throw new NotFoundException('Deck not found'); - } - - const [updated] = await this.db - .update(decks) - .set({ - ...dto, - updatedAt: new Date(), - }) - .where(eq(decks.id, id)) - .returning(); - - return updated; - } - - async remove(id: string, userId: string) { - // Verify ownership - const existing = await this.db.query.decks.findFirst({ - where: and(eq(decks.id, id), eq(decks.userId, userId)), - }); - - if (!existing) { - throw new NotFoundException('Deck not found'); - } - - await this.db.delete(decks).where(eq(decks.id, id)); - - return { success: true }; - } - - async verifyOwnership(id: string, userId: string): Promise { - const deck = await this.db.query.decks.findFirst({ - where: and(eq(decks.id, id), eq(decks.userId, userId)), - }); - return !!deck; - } -} diff --git a/apps/presi/apps/backend/src/instrument.ts b/apps/presi/apps/backend/src/instrument.ts deleted file mode 100644 index 5524856e4..000000000 --- a/apps/presi/apps/backend/src/instrument.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { initErrorTracking } from '@manacore/shared-error-tracking'; - -initErrorTracking({ - serviceName: 'presi-backend', - environment: process.env.NODE_ENV, - release: process.env.APP_VERSION, - debug: process.env.NODE_ENV === 'development', -}); diff --git a/apps/presi/apps/backend/src/main.ts b/apps/presi/apps/backend/src/main.ts deleted file mode 100644 index 1c33ab948..000000000 --- a/apps/presi/apps/backend/src/main.ts +++ /dev/null @@ -1,11 +0,0 @@ -import './instrument'; -import { bootstrapApp } from '@manacore/shared-nestjs-setup'; -import { AppModule } from './app.module'; - -bootstrapApp(AppModule, { - defaultPort: 3008, - serviceName: 'Presi', - additionalCorsOrigins: ['http://localhost:5177', 'http://localhost:5178'], - excludeFromPrefix: [], - swagger: true, -}); diff --git a/apps/presi/apps/backend/src/share/__tests__/share.controller.spec.ts b/apps/presi/apps/backend/src/share/__tests__/share.controller.spec.ts deleted file mode 100644 index 09fd3c947..000000000 --- a/apps/presi/apps/backend/src/share/__tests__/share.controller.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { ShareController } from '../share.controller'; - -const TEST_USER_ID = 'test-user-123'; -const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' }; - -describe('ShareController', () => { - let controller: ShareController; - let service: any; - - beforeEach(() => { - service = { - findByShareCode: jest.fn(), - createShare: jest.fn(), - getSharesForDeck: jest.fn(), - deleteShare: jest.fn(), - }; - controller = new ShareController(service); - }); - - afterEach(() => jest.clearAllMocks()); - - describe('getSharedDeck', () => { - it('should return shared deck by code', async () => { - const deck = { id: 'deck-1', title: 'Shared', slides: [] }; - service.findByShareCode.mockResolvedValue(deck); - - const result = await controller.getSharedDeck('abc123'); - - expect(result).toEqual(deck); - expect(service.findByShareCode).toHaveBeenCalledWith('abc123'); - }); - }); - - describe('createShare', () => { - it('should create share link', async () => { - const share = { id: 'share-1', shareCode: 'abc123', deckId: 'deck-1' }; - service.createShare.mockResolvedValue(share); - - const result = await controller.createShare('deck-1', {} as any, mockUser as any); - - expect(result).toEqual(share); - expect(service.createShare).toHaveBeenCalledWith('deck-1', TEST_USER_ID, undefined); - }); - - it('should create share link with expiration', async () => { - const share = { id: 'share-1', shareCode: 'abc123', expiresAt: '2026-12-31' }; - service.createShare.mockResolvedValue(share); - - const result = await controller.createShare( - 'deck-1', - { expiresAt: '2026-12-31T00:00:00.000Z' } as any, - mockUser as any - ); - - expect(result).toEqual(share); - }); - }); - - describe('getSharesForDeck', () => { - it('should return shares for deck', async () => { - const shares = [{ id: 'share-1', shareCode: 'abc123' }]; - service.getSharesForDeck.mockResolvedValue(shares); - - const result = await controller.getSharesForDeck('deck-1', mockUser as any); - - expect(result).toEqual(shares); - expect(service.getSharesForDeck).toHaveBeenCalledWith('deck-1', TEST_USER_ID); - }); - }); - - describe('deleteShare', () => { - it('should delete share and return success', async () => { - service.deleteShare.mockResolvedValue({ success: true }); - - const result = await controller.deleteShare('share-1', mockUser as any); - - expect(result).toEqual({ success: true }); - expect(service.deleteShare).toHaveBeenCalledWith('share-1', TEST_USER_ID); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/share/__tests__/share.service.spec.ts b/apps/presi/apps/backend/src/share/__tests__/share.service.spec.ts deleted file mode 100644 index e82ab35f8..000000000 --- a/apps/presi/apps/backend/src/share/__tests__/share.service.spec.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { NotFoundException, ForbiddenException } from '@nestjs/common'; -import { ShareService } from '../share.service'; -import { DeckService } from '../../deck/deck.service'; -import { DATABASE_CONNECTION } from '../../db/database.module'; - -const TEST_USER_ID = 'user-1'; -const TEST_DECK_ID = 'deck-1'; -const TEST_SHARE_ID = 'share-1'; -const TEST_SHARE_CODE = 'abc123def456'; - -function createMockShare(overrides: Record = {}) { - return { - id: TEST_SHARE_ID, - deckId: TEST_DECK_ID, - shareCode: TEST_SHARE_CODE, - expiresAt: null, - createdAt: new Date('2025-01-01'), - ...overrides, - }; -} - -describe('ShareService', () => { - let service: ShareService; - let mockDb: any; - let mockDeckService: any; - - beforeEach(async () => { - mockDb = { - query: { - sharedDecks: { - findFirst: jest.fn(), - findMany: jest.fn(), - }, - }, - insert: jest.fn().mockReturnThis(), - values: jest.fn().mockReturnThis(), - returning: jest.fn(), - delete: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - }; - - mockDeckService = { - verifyOwnership: jest.fn(), - }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ShareService, - { - provide: DATABASE_CONNECTION, - useValue: mockDb, - }, - { - provide: DeckService, - useValue: mockDeckService, - }, - ], - }).compile(); - - service = module.get(ShareService); - }); - - describe('createShare', () => { - it('should create a new share link when user owns deck', async () => { - const share = createMockShare(); - mockDeckService.verifyOwnership.mockResolvedValue(true); - mockDb.query.sharedDecks.findFirst.mockResolvedValue(null); // no existing share - mockDb.returning.mockResolvedValue([share]); - - const result = await service.createShare(TEST_DECK_ID, TEST_USER_ID); - - expect(result).toEqual(share); - expect(mockDb.insert).toHaveBeenCalled(); - }); - - it('should return existing valid share instead of creating new one', async () => { - const existingShare = createMockShare(); - mockDeckService.verifyOwnership.mockResolvedValue(true); - mockDb.query.sharedDecks.findFirst.mockResolvedValue(existingShare); - - const result = await service.createShare(TEST_DECK_ID, TEST_USER_ID); - - expect(result).toEqual(existingShare); - expect(mockDb.insert).not.toHaveBeenCalled(); - }); - - it('should throw ForbiddenException when user does not own deck', async () => { - mockDeckService.verifyOwnership.mockResolvedValue(false); - - await expect(service.createShare(TEST_DECK_ID, 'other-user')).rejects.toThrow( - ForbiddenException - ); - }); - - it('should create share with expiration date', async () => { - const expiresAt = new Date('2026-12-31'); - const share = createMockShare({ expiresAt }); - mockDeckService.verifyOwnership.mockResolvedValue(true); - mockDb.query.sharedDecks.findFirst.mockResolvedValue(null); - mockDb.returning.mockResolvedValue([share]); - - const result = await service.createShare(TEST_DECK_ID, TEST_USER_ID, expiresAt); - - expect(result.expiresAt).toEqual(expiresAt); - }); - }); - - describe('findByShareCode', () => { - it('should return deck when share code is valid', async () => { - const deck = { - id: TEST_DECK_ID, - title: 'Shared Deck', - slides: [], - theme: null, - }; - mockDb.query.sharedDecks.findFirst.mockResolvedValue({ - ...createMockShare(), - deck, - }); - - const result = await service.findByShareCode(TEST_SHARE_CODE); - - expect(result).toEqual(deck); - }); - - it('should throw NotFoundException when share code not found', async () => { - mockDb.query.sharedDecks.findFirst.mockResolvedValue(null); - - await expect(service.findByShareCode('invalid-code')).rejects.toThrow(NotFoundException); - }); - - it('should throw NotFoundException when share has expired', async () => { - mockDb.query.sharedDecks.findFirst.mockResolvedValue(null); // expired shares are filtered in query - - await expect(service.findByShareCode(TEST_SHARE_CODE)).rejects.toThrow(NotFoundException); - }); - }); - - describe('getSharesForDeck', () => { - it('should return shares when user owns deck', async () => { - const shares = [createMockShare(), createMockShare({ id: 'share-2', shareCode: 'xyz789' })]; - mockDeckService.verifyOwnership.mockResolvedValue(true); - mockDb.query.sharedDecks.findMany.mockResolvedValue(shares); - - const result = await service.getSharesForDeck(TEST_DECK_ID, TEST_USER_ID); - - expect(result).toEqual(shares); - expect(result).toHaveLength(2); - }); - - it('should throw ForbiddenException when user does not own deck', async () => { - mockDeckService.verifyOwnership.mockResolvedValue(false); - - await expect(service.getSharesForDeck(TEST_DECK_ID, 'other-user')).rejects.toThrow( - ForbiddenException - ); - }); - }); - - describe('deleteShare', () => { - it('should delete share when user owns deck', async () => { - mockDb.query.sharedDecks.findFirst.mockResolvedValue({ - ...createMockShare(), - deck: { userId: TEST_USER_ID }, - }); - mockDb.where.mockResolvedValue(undefined); - - const result = await service.deleteShare(TEST_SHARE_ID, TEST_USER_ID); - - expect(result).toEqual({ success: true }); - }); - - it('should throw NotFoundException when share not found', async () => { - mockDb.query.sharedDecks.findFirst.mockResolvedValue(null); - - await expect(service.deleteShare('nonexistent', TEST_USER_ID)).rejects.toThrow( - NotFoundException - ); - }); - - it('should throw ForbiddenException when user does not own deck', async () => { - mockDb.query.sharedDecks.findFirst.mockResolvedValue({ - ...createMockShare(), - deck: { userId: 'other-user' }, - }); - - await expect(service.deleteShare(TEST_SHARE_ID, TEST_USER_ID)).rejects.toThrow( - ForbiddenException - ); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/share/share.controller.ts b/apps/presi/apps/backend/src/share/share.controller.ts deleted file mode 100644 index dc477e9bb..000000000 --- a/apps/presi/apps/backend/src/share/share.controller.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - Controller, - Get, - Post, - Delete, - Body, - Param, - UseGuards, - ParseUUIDPipe, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { ShareService } from './share.service'; -import { CreateShareDto } from './share.dto'; -import { JwtAuthGuard, CurrentUser } from '@manacore/shared-nestjs-auth'; -import type { CurrentUserData } from '@manacore/shared-nestjs-auth'; - -@ApiTags('Share') -@Controller('share') -export class ShareController { - constructor(private readonly shareService: ShareService) {} - - @Get(':code') - async getSharedDeck(@Param('code') code: string) { - return this.shareService.findByShareCode(code); - } - - @ApiBearerAuth() - @Post('deck/:deckId') - @UseGuards(JwtAuthGuard) - async createShare( - @Param('deckId', ParseUUIDPipe) deckId: string, - @Body() createShareDto: CreateShareDto, - @CurrentUser() user: CurrentUserData - ) { - const expiresAt = createShareDto.expiresAt ? new Date(createShareDto.expiresAt) : undefined; - return this.shareService.createShare(deckId, user.userId, expiresAt); - } - - @ApiBearerAuth() - @Get('deck/:deckId/links') - @UseGuards(JwtAuthGuard) - async getSharesForDeck( - @Param('deckId', ParseUUIDPipe) deckId: string, - @CurrentUser() user: CurrentUserData - ) { - return this.shareService.getSharesForDeck(deckId, user.userId); - } - - @ApiBearerAuth() - @Delete(':shareId') - @UseGuards(JwtAuthGuard) - async deleteShare( - @Param('shareId', ParseUUIDPipe) shareId: string, - @CurrentUser() user: CurrentUserData - ) { - return this.shareService.deleteShare(shareId, user.userId); - } -} diff --git a/apps/presi/apps/backend/src/share/share.dto.ts b/apps/presi/apps/backend/src/share/share.dto.ts deleted file mode 100644 index 118ca2d98..000000000 --- a/apps/presi/apps/backend/src/share/share.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsOptional, IsDateString } from 'class-validator'; - -export class CreateShareDto { - @IsOptional() - @IsDateString() - expiresAt?: string; -} diff --git a/apps/presi/apps/backend/src/share/share.module.ts b/apps/presi/apps/backend/src/share/share.module.ts deleted file mode 100644 index 0c0ea60a6..000000000 --- a/apps/presi/apps/backend/src/share/share.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ShareController } from './share.controller'; -import { ShareService } from './share.service'; -import { DeckModule } from '../deck/deck.module'; - -@Module({ - imports: [DeckModule], - controllers: [ShareController], - providers: [ShareService], - exports: [ShareService], -}) -export class ShareModule {} diff --git a/apps/presi/apps/backend/src/share/share.service.ts b/apps/presi/apps/backend/src/share/share.service.ts deleted file mode 100644 index f002ab9bc..000000000 --- a/apps/presi/apps/backend/src/share/share.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Injectable, Inject, NotFoundException, ForbiddenException } from '@nestjs/common'; -import { eq, and, gt, or, isNull } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import { Database } from '../db/connection'; -import { sharedDecks, slides } from '../db/schema'; -import { DeckService } from '../deck/deck.service'; -import { randomBytes } from 'crypto'; - -@Injectable() -export class ShareService { - constructor( - @Inject(DATABASE_CONNECTION) - private readonly db: Database, - private readonly deckService: DeckService - ) {} - - private generateShareCode(): string { - return randomBytes(6).toString('hex'); // 12 character code - } - - async createShare(deckId: string, userId: string, expiresAt?: Date) { - // Verify ownership - const isOwner = await this.deckService.verifyOwnership(deckId, userId); - if (!isOwner) { - throw new ForbiddenException('You do not own this deck'); - } - - // Check if there's already a valid share - const existingShare = await this.db.query.sharedDecks.findFirst({ - where: and( - eq(sharedDecks.deckId, deckId), - or(isNull(sharedDecks.expiresAt), gt(sharedDecks.expiresAt, new Date())) - ), - }); - - if (existingShare) { - return existingShare; - } - - // Create new share - const [share] = await this.db - .insert(sharedDecks) - .values({ - deckId, - shareCode: this.generateShareCode(), - expiresAt: expiresAt || null, - }) - .returning(); - - return share; - } - - async findByShareCode(shareCode: string) { - const share = await this.db.query.sharedDecks.findFirst({ - where: and( - eq(sharedDecks.shareCode, shareCode), - or(isNull(sharedDecks.expiresAt), gt(sharedDecks.expiresAt, new Date())) - ), - with: { - deck: { - with: { - slides: { - orderBy: [slides.order], - }, - theme: true, - }, - }, - }, - }); - - if (!share) { - throw new NotFoundException('Shared deck not found or link has expired'); - } - - return share.deck; - } - - async getSharesForDeck(deckId: string, userId: string) { - // Verify ownership - const isOwner = await this.deckService.verifyOwnership(deckId, userId); - if (!isOwner) { - throw new ForbiddenException('You do not own this deck'); - } - - return this.db.query.sharedDecks.findMany({ - where: eq(sharedDecks.deckId, deckId), - }); - } - - async deleteShare(shareId: string, userId: string) { - const share = await this.db.query.sharedDecks.findFirst({ - where: eq(sharedDecks.id, shareId), - with: { - deck: true, - }, - }); - - if (!share) { - throw new NotFoundException('Share not found'); - } - - // Verify ownership of the deck - if (share.deck.userId !== userId) { - throw new ForbiddenException('You do not own this deck'); - } - - await this.db.delete(sharedDecks).where(eq(sharedDecks.id, shareId)); - - return { success: true }; - } -} diff --git a/apps/presi/apps/backend/src/slide/__tests__/slide.controller.spec.ts b/apps/presi/apps/backend/src/slide/__tests__/slide.controller.spec.ts deleted file mode 100644 index ec4dd7e09..000000000 --- a/apps/presi/apps/backend/src/slide/__tests__/slide.controller.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { SlideController } from '../slide.controller'; - -const TEST_USER_ID = 'test-user-123'; -const mockUser = { userId: TEST_USER_ID, email: 'test@example.com' }; - -describe('SlideController', () => { - let controller: SlideController; - let service: any; - - beforeEach(() => { - service = { - create: jest.fn(), - update: jest.fn(), - remove: jest.fn(), - reorder: jest.fn(), - }; - controller = new SlideController(service); - }); - - afterEach(() => jest.clearAllMocks()); - - describe('create', () => { - it('should create slide for deck', async () => { - const slide = { id: 'slide-1', deckId: 'deck-1', order: 0 }; - service.create.mockResolvedValue(slide); - - const result = await controller.create( - 'deck-1', - { content: { type: 'title' as const, title: 'Hello' } }, - mockUser as any - ); - - expect(result).toEqual(slide); - expect(service.create).toHaveBeenCalledWith('deck-1', TEST_USER_ID, { - content: { type: 'title', title: 'Hello' }, - }); - }); - }); - - describe('update', () => { - it('should update slide', async () => { - const slide = { id: 'slide-1', content: { type: 'content', body: 'Updated' } }; - service.update.mockResolvedValue(slide); - - const result = await controller.update( - 'slide-1', - { content: { type: 'content' as const, body: 'Updated' } }, - mockUser as any - ); - - expect(result).toEqual(slide); - expect(service.update).toHaveBeenCalledWith('slide-1', TEST_USER_ID, { - content: { type: 'content', body: 'Updated' }, - }); - }); - }); - - describe('remove', () => { - it('should delete slide', async () => { - service.remove.mockResolvedValue({ success: true }); - - const result = await controller.remove('slide-1', mockUser as any); - - expect(result).toEqual({ success: true }); - expect(service.remove).toHaveBeenCalledWith('slide-1', TEST_USER_ID); - }); - }); - - describe('reorder', () => { - it('should reorder slides', async () => { - service.reorder.mockResolvedValue({ success: true }); - - const dto = { - slides: [ - { id: 'slide-1', order: 1 }, - { id: 'slide-2', order: 0 }, - ], - }; - const result = await controller.reorder(dto as any, mockUser as any); - - expect(result).toEqual({ success: true }); - expect(service.reorder).toHaveBeenCalledWith(TEST_USER_ID, dto); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/slide/__tests__/slide.service.spec.ts b/apps/presi/apps/backend/src/slide/__tests__/slide.service.spec.ts deleted file mode 100644 index 7e461fe6c..000000000 --- a/apps/presi/apps/backend/src/slide/__tests__/slide.service.spec.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { NotFoundException, ForbiddenException } from '@nestjs/common'; -import { SlideService } from '../slide.service'; -import { DeckService } from '../../deck/deck.service'; -import { DATABASE_CONNECTION } from '../../db/database.module'; - -const TEST_USER_ID = 'user-1'; -const TEST_DECK_ID = 'deck-1'; -const TEST_SLIDE_ID = 'slide-1'; - -function createMockSlide(overrides: Record = {}) { - return { - id: TEST_SLIDE_ID, - deckId: TEST_DECK_ID, - order: 0, - content: { type: 'title', title: 'Hello World' }, - createdAt: new Date('2025-01-01'), - deck: { userId: TEST_USER_ID }, - ...overrides, - }; -} - -describe('SlideService', () => { - let service: SlideService; - let mockDb: any; - let mockDeckService: any; - - beforeEach(async () => { - mockDb = { - query: { - slides: { - findFirst: jest.fn(), - }, - }, - select: jest.fn().mockReturnThis(), - from: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - insert: jest.fn().mockReturnThis(), - values: jest.fn().mockReturnThis(), - returning: jest.fn(), - update: jest.fn().mockReturnThis(), - set: jest.fn().mockReturnThis(), - delete: jest.fn().mockReturnThis(), - }; - - mockDeckService = { - verifyOwnership: jest.fn(), - }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SlideService, - { - provide: DATABASE_CONNECTION, - useValue: mockDb, - }, - { - provide: DeckService, - useValue: mockDeckService, - }, - ], - }).compile(); - - service = module.get(SlideService); - }); - - describe('create', () => { - it('should create a slide when user owns deck', async () => { - const slide = createMockSlide(); - mockDeckService.verifyOwnership.mockResolvedValue(true); - mockDb.where.mockResolvedValueOnce([{ maxOrder: 2 }]); // max order query - mockDb.returning.mockResolvedValue([slide]); - - const result = await service.create(TEST_DECK_ID, TEST_USER_ID, { - content: { type: 'title' as const, title: 'Hello World' }, - }); - - expect(result).toEqual(slide); - expect(mockDeckService.verifyOwnership).toHaveBeenCalledWith(TEST_DECK_ID, TEST_USER_ID); - }); - - it('should auto-increment order when not provided', async () => { - mockDeckService.verifyOwnership.mockResolvedValue(true); - mockDb.where.mockResolvedValueOnce([{ maxOrder: 5 }]); - mockDb.returning.mockResolvedValue([createMockSlide({ order: 6 })]); - - const result = await service.create(TEST_DECK_ID, TEST_USER_ID, { - content: { type: 'content' as const }, - }); - - expect(result.order).toBe(6); - }); - - it('should throw ForbiddenException when user does not own deck', async () => { - mockDeckService.verifyOwnership.mockResolvedValue(false); - - await expect( - service.create(TEST_DECK_ID, 'other-user', { - content: { type: 'title' as const }, - }) - ).rejects.toThrow(ForbiddenException); - }); - }); - - describe('update', () => { - it('should update slide when user owns deck', async () => { - const slide = createMockSlide(); - const updated = createMockSlide({ content: { type: 'content', body: 'Updated' } }); - mockDb.query.slides.findFirst.mockResolvedValue(slide); - mockDb.returning.mockResolvedValue([updated]); - - const result = await service.update(TEST_SLIDE_ID, TEST_USER_ID, { - content: { type: 'content' as const, body: 'Updated' }, - }); - - expect(result).toEqual(updated); - }); - - it('should throw NotFoundException when slide not found', async () => { - mockDb.query.slides.findFirst.mockResolvedValue(null); - - await expect( - service.update('nonexistent', TEST_USER_ID, { - content: { type: 'title' as const }, - }) - ).rejects.toThrow(NotFoundException); - }); - - it('should throw ForbiddenException when user does not own slide', async () => { - mockDb.query.slides.findFirst.mockResolvedValue( - createMockSlide({ deck: { userId: 'other-user' } }) - ); - - await expect( - service.update(TEST_SLIDE_ID, TEST_USER_ID, { - content: { type: 'title' as const }, - }) - ).rejects.toThrow(ForbiddenException); - }); - }); - - describe('remove', () => { - it('should delete slide and return success', async () => { - mockDb.query.slides.findFirst.mockResolvedValue(createMockSlide()); - mockDb.where.mockResolvedValue(undefined); - - const result = await service.remove(TEST_SLIDE_ID, TEST_USER_ID); - - expect(result).toEqual({ success: true }); - }); - - it('should throw NotFoundException when slide not found', async () => { - mockDb.query.slides.findFirst.mockResolvedValue(null); - - await expect(service.remove('nonexistent', TEST_USER_ID)).rejects.toThrow(NotFoundException); - }); - - it('should throw ForbiddenException when user does not own slide', async () => { - mockDb.query.slides.findFirst.mockResolvedValue( - createMockSlide({ deck: { userId: 'other-user' } }) - ); - - await expect(service.remove(TEST_SLIDE_ID, TEST_USER_ID)).rejects.toThrow(ForbiddenException); - }); - }); - - describe('reorder', () => { - it('should reorder slides when user owns all', async () => { - mockDb.query.slides.findFirst - .mockResolvedValueOnce(createMockSlide({ id: 'slide-1' })) - .mockResolvedValueOnce(createMockSlide({ id: 'slide-2' })); - mockDb.where.mockResolvedValue(undefined); - - const result = await service.reorder(TEST_USER_ID, { - slides: [ - { id: 'slide-1', order: 1 }, - { id: 'slide-2', order: 0 }, - ], - }); - - expect(result).toEqual({ success: true }); - }); - - it('should throw NotFoundException when slide not found during reorder', async () => { - mockDb.query.slides.findFirst.mockResolvedValue(null); - - await expect( - service.reorder(TEST_USER_ID, { - slides: [{ id: 'nonexistent', order: 0 }], - }) - ).rejects.toThrow(NotFoundException); - }); - - it('should throw ForbiddenException when user does not own a slide', async () => { - mockDb.query.slides.findFirst.mockResolvedValue( - createMockSlide({ deck: { userId: 'other-user' } }) - ); - - await expect( - service.reorder(TEST_USER_ID, { - slides: [{ id: 'slide-1', order: 0 }], - }) - ).rejects.toThrow(ForbiddenException); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/slide/slide.controller.ts b/apps/presi/apps/backend/src/slide/slide.controller.ts deleted file mode 100644 index dba91ff58..000000000 --- a/apps/presi/apps/backend/src/slide/slide.controller.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - Controller, - Post, - Put, - Delete, - Body, - Param, - UseGuards, - ParseUUIDPipe, -} from '@nestjs/common'; -import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { SlideService } from './slide.service'; -import { CreateSlideDto } from './slide.dto'; -import type { UpdateSlideDto, ReorderSlidesDto } from './slide.dto'; -import { JwtAuthGuard, CurrentUser } from '@manacore/shared-nestjs-auth'; -import type { CurrentUserData } from '@manacore/shared-nestjs-auth'; - -@ApiTags('Slides') -@ApiBearerAuth() -@Controller() -@UseGuards(JwtAuthGuard) -export class SlideController { - constructor(private readonly slideService: SlideService) {} - - @Post('decks/:deckId/slides') - async create( - @Param('deckId', ParseUUIDPipe) deckId: string, - @Body() createSlideDto: CreateSlideDto, - @CurrentUser() user: CurrentUserData - ) { - return this.slideService.create(deckId, user.userId, createSlideDto); - } - - @Put('slides/:id') - async update( - @Param('id', ParseUUIDPipe) id: string, - @Body() updateSlideDto: UpdateSlideDto, - @CurrentUser() user: CurrentUserData - ) { - return this.slideService.update(id, user.userId, updateSlideDto); - } - - @Delete('slides/:id') - async remove(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: CurrentUserData) { - return this.slideService.remove(id, user.userId); - } - - @Put('slides/reorder') - async reorder(@Body() reorderDto: ReorderSlidesDto, @CurrentUser() user: CurrentUserData) { - return this.slideService.reorder(user.userId, reorderDto); - } -} diff --git a/apps/presi/apps/backend/src/slide/slide.dto.ts b/apps/presi/apps/backend/src/slide/slide.dto.ts deleted file mode 100644 index b48b0fc6a..000000000 --- a/apps/presi/apps/backend/src/slide/slide.dto.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - IsOptional, - IsNumber, - IsArray, - ValidateNested, - IsUUID, - IsIn, - IsString, - IsUrl, - MaxLength, - Min, - IsInt, - ArrayMaxSize, -} from 'class-validator'; -import { Type } from 'class-transformer'; - -class SlideContent { - @IsIn(['title', 'content', 'image', 'split']) - type: 'title' | 'content' | 'image' | 'split'; - - @IsString() - @IsOptional() - @MaxLength(500) - title?: string; - - @IsString() - @IsOptional() - @MaxLength(500) - subtitle?: string; - - @IsString() - @IsOptional() - @MaxLength(5000) - body?: string; - - @IsUrl() - @IsOptional() - imageUrl?: string; - - @IsArray() - @IsString({ each: true }) - @IsOptional() - @ArrayMaxSize(50) - bulletPoints?: string[]; -} - -export class CreateSlideDto { - @ValidateNested() - @Type(() => SlideContent) - content: SlideContent; - - @IsInt() - @Min(0) - @IsOptional() - order?: number; -} - -export class UpdateSlideDto { - @ValidateNested() - @Type(() => SlideContent) - @IsOptional() - content?: SlideContent; - - @IsInt() - @Min(0) - @IsOptional() - order?: number; -} - -class SlideOrderItem { - @IsUUID() - id: string; - - @IsInt() - @Min(0) - order: number; -} - -export class ReorderSlidesDto { - @IsArray() - @ValidateNested({ each: true }) - @Type(() => SlideOrderItem) - @ArrayMaxSize(200) - slides: SlideOrderItem[]; -} diff --git a/apps/presi/apps/backend/src/slide/slide.module.ts b/apps/presi/apps/backend/src/slide/slide.module.ts deleted file mode 100644 index ad785780d..000000000 --- a/apps/presi/apps/backend/src/slide/slide.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SlideController } from './slide.controller'; -import { SlideService } from './slide.service'; -import { DeckModule } from '../deck/deck.module'; - -@Module({ - imports: [DeckModule], - controllers: [SlideController], - providers: [SlideService], -}) -export class SlideModule {} diff --git a/apps/presi/apps/backend/src/slide/slide.service.ts b/apps/presi/apps/backend/src/slide/slide.service.ts deleted file mode 100644 index 0aad844de..000000000 --- a/apps/presi/apps/backend/src/slide/slide.service.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Injectable, Inject, NotFoundException, ForbiddenException } from '@nestjs/common'; -import { eq, max } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import { Database } from '../db/connection'; -import { slides, decks } from '../db/schema'; -import { DeckService } from '../deck/deck.service'; -import { CreateSlideDto } from './slide.dto'; -import type { UpdateSlideDto, ReorderSlidesDto } from './slide.dto'; - -@Injectable() -export class SlideService { - constructor( - @Inject(DATABASE_CONNECTION) - private readonly db: Database, - private readonly deckService: DeckService - ) {} - - async create(deckId: string, userId: string, dto: CreateSlideDto) { - // Verify deck ownership - const isOwner = await this.deckService.verifyOwnership(deckId, userId); - if (!isOwner) { - throw new ForbiddenException('Not authorized to modify this deck'); - } - - // Get next order number - const result = await this.db - .select({ maxOrder: max(slides.order) }) - .from(slides) - .where(eq(slides.deckId, deckId)); - - const nextOrder = dto.order ?? (result[0]?.maxOrder ?? -1) + 1; - - const [slide] = await this.db - .insert(slides) - .values({ - deckId, - order: nextOrder, - content: dto.content, - }) - .returning(); - - // Update deck's updatedAt - await this.db.update(decks).set({ updatedAt: new Date() }).where(eq(decks.id, deckId)); - - return slide; - } - - async update(id: string, userId: string, dto: UpdateSlideDto) { - // Get slide and verify ownership - const slide = await this.db.query.slides.findFirst({ - where: eq(slides.id, id), - with: { deck: true }, - }); - - if (!slide) { - throw new NotFoundException('Slide not found'); - } - - if (slide.deck.userId !== userId) { - throw new ForbiddenException('Not authorized to modify this slide'); - } - - const [updated] = await this.db - .update(slides) - .set({ - content: dto.content ?? slide.content, - order: dto.order ?? slide.order, - }) - .where(eq(slides.id, id)) - .returning(); - - // Update deck's updatedAt - await this.db.update(decks).set({ updatedAt: new Date() }).where(eq(decks.id, slide.deckId)); - - return updated; - } - - async remove(id: string, userId: string) { - // Get slide and verify ownership - const slide = await this.db.query.slides.findFirst({ - where: eq(slides.id, id), - with: { deck: true }, - }); - - if (!slide) { - throw new NotFoundException('Slide not found'); - } - - if (slide.deck.userId !== userId) { - throw new ForbiddenException('Not authorized to delete this slide'); - } - - await this.db.delete(slides).where(eq(slides.id, id)); - - // Update deck's updatedAt - await this.db.update(decks).set({ updatedAt: new Date() }).where(eq(decks.id, slide.deckId)); - - return { success: true }; - } - - async reorder(userId: string, dto: ReorderSlidesDto) { - // Verify ownership of all slides - for (const item of dto.slides) { - const slide = await this.db.query.slides.findFirst({ - where: eq(slides.id, item.id), - with: { deck: true }, - }); - - if (!slide) { - throw new NotFoundException(`Slide ${item.id} not found`); - } - - if (slide.deck.userId !== userId) { - throw new ForbiddenException('Not authorized to reorder these slides'); - } - } - - // Update orders - for (const item of dto.slides) { - await this.db.update(slides).set({ order: item.order }).where(eq(slides.id, item.id)); - } - - return { success: true }; - } -} diff --git a/apps/presi/apps/backend/src/theme/__tests__/theme.controller.spec.ts b/apps/presi/apps/backend/src/theme/__tests__/theme.controller.spec.ts deleted file mode 100644 index e78b4d557..000000000 --- a/apps/presi/apps/backend/src/theme/__tests__/theme.controller.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ThemeController } from '../theme.controller'; - -const mockTheme = { - id: 'theme-1', - name: 'Dark', - colors: { primary: '#000', secondary: '#333', background: '#111', text: '#fff', accent: '#f00' }, - fonts: { heading: 'Arial', body: 'Helvetica' }, - isDefault: false, -}; - -describe('ThemeController', () => { - let controller: ThemeController; - let service: any; - - beforeEach(() => { - service = { - findAll: jest.fn(), - findDefault: jest.fn(), - findOne: jest.fn(), - }; - controller = new ThemeController(service); - }); - - afterEach(() => jest.clearAllMocks()); - - describe('findAll', () => { - it('should return all themes', async () => { - const themes = [mockTheme]; - service.findAll.mockResolvedValue(themes); - - const result = await controller.findAll(); - - expect(result).toEqual(themes); - }); - }); - - describe('findDefault', () => { - it('should return default theme', async () => { - const defaultTheme = { ...mockTheme, isDefault: true }; - service.findDefault.mockResolvedValue(defaultTheme); - - const result = await controller.findDefault(); - - expect(result).toEqual(defaultTheme); - expect(result.isDefault).toBe(true); - }); - - it('should return null when no default theme', async () => { - service.findDefault.mockResolvedValue(null); - - const result = await controller.findDefault(); - - expect(result).toBeNull(); - }); - }); - - describe('findOne', () => { - it('should return theme by id', async () => { - service.findOne.mockResolvedValue(mockTheme); - - const result = await controller.findOne('theme-1'); - - expect(result).toEqual(mockTheme); - expect(service.findOne).toHaveBeenCalledWith('theme-1'); - }); - - it('should return null when not found', async () => { - service.findOne.mockResolvedValue(null); - - const result = await controller.findOne('nonexistent'); - - expect(result).toBeNull(); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/theme/__tests__/theme.service.spec.ts b/apps/presi/apps/backend/src/theme/__tests__/theme.service.spec.ts deleted file mode 100644 index 9f8008ea2..000000000 --- a/apps/presi/apps/backend/src/theme/__tests__/theme.service.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ThemeService } from '../theme.service'; -import { DATABASE_CONNECTION } from '../../db/database.module'; - -const mockTheme = { - id: 'theme-1', - name: 'Dark', - colors: { primary: '#000', secondary: '#333', background: '#111', text: '#fff', accent: '#f00' }, - fonts: { heading: 'Arial', body: 'Helvetica' }, - isDefault: false, -}; - -const defaultTheme = { - ...mockTheme, - id: 'theme-default', - name: 'Default', - isDefault: true, -}; - -describe('ThemeService', () => { - let service: ThemeService; - let mockDb: any; - - beforeEach(async () => { - mockDb = { - select: jest.fn().mockReturnThis(), - from: jest.fn().mockReturnThis(), - where: jest.fn(), - }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ThemeService, - { - provide: DATABASE_CONNECTION, - useValue: mockDb, - }, - ], - }).compile(); - - service = module.get(ThemeService); - }); - - describe('findAll', () => { - it('should return all themes', async () => { - const themes = [mockTheme, defaultTheme]; - mockDb.from.mockResolvedValue(themes); - - const result = await service.findAll(); - - expect(result).toEqual(themes); - expect(result).toHaveLength(2); - }); - - it('should return empty array when no themes exist', async () => { - mockDb.from.mockResolvedValue([]); - - const result = await service.findAll(); - - expect(result).toEqual([]); - }); - }); - - describe('findOne', () => { - it('should return theme when found', async () => { - mockDb.where.mockResolvedValue([mockTheme]); - - const result = await service.findOne('theme-1'); - - expect(result).toEqual(mockTheme); - }); - - it('should return null when theme not found', async () => { - mockDb.where.mockResolvedValue([]); - - const result = await service.findOne('nonexistent'); - - expect(result).toBeNull(); - }); - }); - - describe('findDefault', () => { - it('should return default theme', async () => { - mockDb.where.mockResolvedValue([defaultTheme]); - - const result = await service.findDefault(); - - expect(result).toEqual(defaultTheme); - expect(result.isDefault).toBe(true); - }); - - it('should return null when no default theme exists', async () => { - mockDb.where.mockResolvedValue([]); - - const result = await service.findDefault(); - - expect(result).toBeNull(); - }); - }); -}); diff --git a/apps/presi/apps/backend/src/theme/theme.controller.ts b/apps/presi/apps/backend/src/theme/theme.controller.ts deleted file mode 100644 index e64ea341b..000000000 --- a/apps/presi/apps/backend/src/theme/theme.controller.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { ThemeService } from './theme.service'; - -@ApiTags('Themes') -@Controller('themes') -export class ThemeController { - constructor(private readonly themeService: ThemeService) {} - - @Get() - async findAll() { - return this.themeService.findAll(); - } - - @Get('default') - async findDefault() { - return this.themeService.findDefault(); - } - - @Get(':id') - async findOne(@Param('id', ParseUUIDPipe) id: string) { - return this.themeService.findOne(id); - } -} diff --git a/apps/presi/apps/backend/src/theme/theme.module.ts b/apps/presi/apps/backend/src/theme/theme.module.ts deleted file mode 100644 index 5deba376f..000000000 --- a/apps/presi/apps/backend/src/theme/theme.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ThemeController } from './theme.controller'; -import { ThemeService } from './theme.service'; - -@Module({ - controllers: [ThemeController], - providers: [ThemeService], - exports: [ThemeService], -}) -export class ThemeModule {} diff --git a/apps/presi/apps/backend/src/theme/theme.service.ts b/apps/presi/apps/backend/src/theme/theme.service.ts deleted file mode 100644 index 33083d315..000000000 --- a/apps/presi/apps/backend/src/theme/theme.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable, Inject } from '@nestjs/common'; -import { eq } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import { Database } from '../db/connection'; -import { themes } from '../db/schema'; - -@Injectable() -export class ThemeService { - constructor( - @Inject(DATABASE_CONNECTION) - private readonly db: Database - ) {} - - async findAll() { - return this.db.select().from(themes); - } - - async findOne(id: string) { - const result = await this.db.select().from(themes).where(eq(themes.id, id)); - return result[0] || null; - } - - async findDefault() { - const result = await this.db.select().from(themes).where(eq(themes.isDefault, true)); - return result[0] || null; - } -} diff --git a/apps/presi/apps/backend/tsconfig.json b/apps/presi/apps/backend/tsconfig.json deleted file mode 100644 index 27971033a..000000000 --- a/apps/presi/apps/backend/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "moduleResolution": "node", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "outDir": "./dist", - "baseUrl": "./", - "rootDir": "./src", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/apps/presi/apps/server/package.json b/apps/presi/apps/server/package.json new file mode 100644 index 000000000..b0c79182c --- /dev/null +++ b/apps/presi/apps/server/package.json @@ -0,0 +1,22 @@ +{ + "name": "@presi/server", + "version": "0.1.0", + "private": true, + "description": "Presi server-side compute (Hono + Bun) — share links, admin/GDPR", + "type": "module", + "scripts": { + "dev": "bun run --watch src/index.ts", + "start": "bun run src/index.ts", + "type-check": "bun x tsc --noEmit" + }, + "dependencies": { + "@manacore/shared-hono": "workspace:*", + "drizzle-orm": "^0.45.1", + "hono": "^4.7.0", + "postgres": "^3.4.5" + }, + "devDependencies": { + "@types/bun": "^1.2.0", + "typescript": "^5.9.3" + } +} diff --git a/apps/presi/apps/server/src/db/index.ts b/apps/presi/apps/server/src/db/index.ts new file mode 100644 index 000000000..b7b61eb58 --- /dev/null +++ b/apps/presi/apps/server/src/db/index.ts @@ -0,0 +1,98 @@ +/** + * Database schema — only tables needed by server-side share endpoints. + * Deck/slide CRUD is handled client-side via local-first + mana-sync. + */ + +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { + pgTable, + uuid, + text, + boolean, + timestamp, + integer, + jsonb, + index, +} from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +const DATABASE_URL = + process.env.DATABASE_URL ?? 'postgresql://manacore:devpassword@localhost:5432/presi'; + +const connection = postgres(DATABASE_URL, { + max: 5, + idle_timeout: 20, +}); + +// ─── Schema (read-only for share lookups) ──────────────── + +export const decks = pgTable('decks', { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id').notNull(), + title: text('title').notNull(), + description: text('description'), + themeId: uuid('theme_id'), + isPublic: boolean('is_public').default(false).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); + +export const slides = pgTable( + 'slides', + { + id: uuid('id').primaryKey().defaultRandom(), + deckId: uuid('deck_id').notNull(), + order: integer('order').default(0).notNull(), + content: jsonb('content'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [index('slides_deck_order_idx').on(table.deckId, table.order)] +); + +export const themes = pgTable('themes', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + colors: jsonb('colors'), + fonts: jsonb('fonts'), + isDefault: boolean('is_default').default(false), +}); + +export const sharedDecks = pgTable( + 'shared_decks', + { + id: uuid('id').primaryKey().defaultRandom(), + deckId: uuid('deck_id').notNull(), + shareCode: text('share_code').notNull().unique(), + expiresAt: timestamp('expires_at', { withTimezone: true }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [index('shared_decks_deck_id_idx').on(table.deckId)] +); + +export const decksRelations = relations(decks, ({ many }) => ({ + slides: many(slides), + sharedDecks: many(sharedDecks), +})); + +export const slidesRelations = relations(slides, ({ one }) => ({ + deck: one(decks, { fields: [slides.deckId], references: [decks.id] }), +})); + +export const sharedDecksRelations = relations(sharedDecks, ({ one }) => ({ + deck: one(decks, { fields: [sharedDecks.deckId], references: [decks.id] }), +})); + +export const db = drizzle(connection, { + schema: { + decks, + slides, + themes, + sharedDecks, + decksRelations, + slidesRelations, + sharedDecksRelations, + }, +}); + +export type Database = typeof db; diff --git a/apps/presi/apps/server/src/index.ts b/apps/presi/apps/server/src/index.ts new file mode 100644 index 000000000..2abc8df8f --- /dev/null +++ b/apps/presi/apps/server/src/index.ts @@ -0,0 +1,58 @@ +/** + * Presi Server — Hono + Bun + * + * Lightweight server for compute-only endpoints: + * - Share links (public deck viewing + link management) + * - Admin (GDPR compliance) + * + * All CRUD (decks, slides, themes) is handled client-side via local-first + sync. + */ + +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import { errorHandler, notFoundHandler } from '@manacore/shared-hono/error'; +import { healthRoute } from '@manacore/shared-hono/health'; +import { adminRoutes } from '@manacore/shared-hono/admin'; +import { shareRoutes } from './routes/share'; +import { db, decks, slides, sharedDecks } from './db'; + +const app = new Hono(); + +// Error handling +app.onError(errorHandler); +app.notFound(notFoundHandler); + +// Middleware +app.use('*', logger()); +app.use( + '*', + cors({ + origin: (process.env.CORS_ORIGINS ?? 'http://localhost:5178,http://localhost:5173').split(','), + allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowHeaders: ['Authorization', 'Content-Type', 'X-Service-Key'], + credentials: true, + }) +); + +// Routes +app.route('/health', healthRoute('presi-server')); +app.route('/api/share', shareRoutes); +app.route( + '/api/v1/admin', + adminRoutes(db, [ + { table: sharedDecks, name: 'sharedDecks', userIdColumn: sharedDecks.deckId }, + { table: slides, name: 'slides', userIdColumn: slides.deckId }, + { table: decks, name: 'decks', userIdColumn: decks.userId }, + ]) +); + +// Start +const port = Number(process.env.PORT ?? 3008); + +console.log(`Presi server (Hono + Bun) starting on port ${port}`); + +export default { + port, + fetch: app.fetch, +}; diff --git a/apps/presi/apps/server/src/routes/share.ts b/apps/presi/apps/server/src/routes/share.ts new file mode 100644 index 000000000..26b770114 --- /dev/null +++ b/apps/presi/apps/server/src/routes/share.ts @@ -0,0 +1,161 @@ +/** + * Share routes — public and authenticated share link management. + * + * Public: GET /share/:code — view shared deck (no auth) + * Auth: POST /share/deck/:deckId — create share link + * GET /share/deck/:deckId/links — list share links + * DELETE /share/:shareId — delete share link + */ + +import { Hono } from 'hono'; +import { eq, and, gt, or, isNull, asc } from 'drizzle-orm'; +import { HTTPException } from 'hono/http-exception'; +import { authMiddleware } from '@manacore/shared-hono/auth'; +import { db, sharedDecks, decks, slides, themes } from '../db'; + +const shareRoutes = new Hono(); + +/** Generate a 12-character share code. */ +function generateShareCode(): string { + const bytes = new Uint8Array(6); + crypto.getRandomValues(bytes); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +// ─── Public endpoint (no auth) ────────────────────────── + +/** Get a shared deck by share code. */ +shareRoutes.get('/:code', async (c) => { + const code = c.req.param('code'); + + const share = await db.query.sharedDecks.findFirst({ + where: and( + eq(sharedDecks.shareCode, code), + or(isNull(sharedDecks.expiresAt), gt(sharedDecks.expiresAt, new Date())) + ), + }); + + if (!share) { + throw new HTTPException(404, { message: 'Shared deck not found or link has expired' }); + } + + // Load deck with slides and theme + const deck = await db.query.decks.findFirst({ + where: eq(decks.id, share.deckId), + }); + + if (!deck) { + throw new HTTPException(404, { message: 'Deck not found' }); + } + + const deckSlides = await db.query.slides.findMany({ + where: eq(slides.deckId, deck.id), + orderBy: [asc(slides.order)], + }); + + let theme = null; + if (deck.themeId) { + theme = await db.query.themes.findFirst({ + where: eq(themes.id, deck.themeId), + }); + } + + return c.json({ + ...deck, + slides: deckSlides, + theme, + }); +}); + +// ─── Authenticated endpoints ──────────────────────────── + +shareRoutes.use('/deck/*', authMiddleware()); + +/** Create a share link for a deck. */ +shareRoutes.post('/deck/:deckId', async (c) => { + const userId = c.get('userId'); + const deckId = c.req.param('deckId'); + + // Verify ownership + const deck = await db.query.decks.findFirst({ + where: and(eq(decks.id, deckId), eq(decks.userId, userId)), + }); + if (!deck) { + throw new HTTPException(403, { message: 'You do not own this deck' }); + } + + // Check for existing valid share + const existing = await db.query.sharedDecks.findFirst({ + where: and( + eq(sharedDecks.deckId, deckId), + or(isNull(sharedDecks.expiresAt), gt(sharedDecks.expiresAt, new Date())) + ), + }); + + if (existing) { + return c.json(existing); + } + + // Parse optional expiry + const body = await c.req.json<{ expiresAt?: string }>().catch(() => ({})); + + const [share] = await db + .insert(sharedDecks) + .values({ + deckId, + shareCode: generateShareCode(), + expiresAt: body.expiresAt ? new Date(body.expiresAt) : null, + }) + .returning(); + + return c.json(share, 201); +}); + +/** List share links for a deck. */ +shareRoutes.get('/deck/:deckId/links', async (c) => { + const userId = c.get('userId'); + const deckId = c.req.param('deckId'); + + // Verify ownership + const deck = await db.query.decks.findFirst({ + where: and(eq(decks.id, deckId), eq(decks.userId, userId)), + }); + if (!deck) { + throw new HTTPException(403, { message: 'You do not own this deck' }); + } + + const links = await db.query.sharedDecks.findMany({ + where: eq(sharedDecks.deckId, deckId), + }); + + return c.json(links); +}); + +/** Delete a share link. */ +shareRoutes.delete('/:shareId', authMiddleware(), async (c) => { + const userId = c.get('userId'); + const shareId = c.req.param('shareId'); + + const share = await db.query.sharedDecks.findFirst({ + where: eq(sharedDecks.id, shareId), + }); + + if (!share) { + throw new HTTPException(404, { message: 'Share not found' }); + } + + // Verify ownership of the deck + const deck = await db.query.decks.findFirst({ + where: eq(decks.id, share.deckId), + }); + if (!deck || deck.userId !== userId) { + throw new HTTPException(403, { message: 'You do not own this deck' }); + } + + await db.delete(sharedDecks).where(eq(sharedDecks.id, shareId)); + return c.json({ success: true }); +}); + +export { shareRoutes }; diff --git a/apps/presi/apps/server/tsconfig.json b/apps/presi/apps/server/tsconfig.json new file mode 100644 index 000000000..d89257d10 --- /dev/null +++ b/apps/presi/apps/server/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["bun"] + }, + "include": ["src/**/*.ts"] +} diff --git a/apps/presi/apps/web/src/lib/api/client.ts b/apps/presi/apps/web/src/lib/api/client.ts index 457796f7d..f17320313 100644 --- a/apps/presi/apps/web/src/lib/api/client.ts +++ b/apps/presi/apps/web/src/lib/api/client.ts @@ -1,25 +1,25 @@ +/** + * Presi API Client — Share endpoints only. + * + * All CRUD (decks, slides, themes) is handled via local-first + mana-sync. + * This client only handles share links which require server-side state. + */ + import { browser } from '$app/environment'; -import { PUBLIC_BACKEND_URL, PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; -import type { - Deck, - Slide, - CreateDeckDto, - UpdateDeckDto, - CreateSlideDto, - UpdateSlideDto, - ReorderSlidesDto, -} from '@presi/shared'; -const BASE_URL = PUBLIC_BACKEND_URL || 'http://localhost:3008'; -const API_URL = `${BASE_URL}/api`; -const AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; - -// Storage keys must match @manacore/shared-auth const STORAGE_KEYS = { APP_TOKEN: '@auth/appToken', - REFRESH_TOKEN: '@auth/refreshToken', }; +function getServerUrl(): string { + if (browser) { + const injected = (window as unknown as { __PUBLIC_PRESI_SERVER_URL__?: string }) + .__PUBLIC_PRESI_SERVER_URL__; + if (injected) return injected; + } + return import.meta.env.PUBLIC_PRESI_SERVER_URL || 'http://localhost:3008'; +} + function getToken(): string | null { if (!browser) return null; return localStorage.getItem(STORAGE_KEYS.APP_TOKEN); @@ -27,202 +27,16 @@ function getToken(): string | null { async function fetchWithAuth(url: string, options: RequestInit = {}): Promise { const token = getToken(); - const headers: HeadersInit = { 'Content-Type': 'application/json', ...options.headers, }; - if (token) { (headers as Record)['Authorization'] = `Bearer ${token}`; } - - const response = await fetch(url, { - ...options, - headers, - }); - - if (response.status === 401) { - // Token expired - try to refresh - const refreshed = await refreshToken(); - if (refreshed) { - // Retry the request with new token - const newToken = getToken(); - if (newToken) { - (headers as Record)['Authorization'] = `Bearer ${newToken}`; - } - return fetch(url, { ...options, headers }); - } - // Clear tokens and redirect to login - if (browser) { - localStorage.removeItem(STORAGE_KEYS.APP_TOKEN); - localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN); - window.location.href = '/login'; - } - } - - return response; + return fetch(url, { ...options, headers }); } -async function refreshToken(): Promise { - if (!browser) return false; - - const storedRefreshToken = localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN); - if (!storedRefreshToken) return false; - - try { - const response = await fetch(`${AUTH_URL}/api/v1/auth/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken: storedRefreshToken }), - }); - - if (response.ok) { - const data = await response.json(); - localStorage.setItem(STORAGE_KEYS.APP_TOKEN, data.accessToken); - if (data.refreshToken) { - localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, data.refreshToken); - } - return true; - } - } catch (e) { - console.error('Failed to refresh token:', e); - } - - return false; -} - -// Auth API (legacy - prefer using @manacore/shared-auth via auth store) -export const authApi = { - async login(email: string, password: string) { - const response = await fetch(`${AUTH_URL}/api/v1/auth/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || 'Login failed'); - } - - const data = await response.json(); - if (browser) { - localStorage.setItem(STORAGE_KEYS.APP_TOKEN, data.accessToken); - localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, data.refreshToken); - } - return data; - }, - - async register(email: string, password: string) { - const response = await fetch(`${AUTH_URL}/api/v1/auth/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || 'Registration failed'); - } - - const data = await response.json(); - if (browser) { - localStorage.setItem(STORAGE_KEYS.APP_TOKEN, data.accessToken); - localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, data.refreshToken); - } - return data; - }, - - logout() { - if (browser) { - localStorage.removeItem(STORAGE_KEYS.APP_TOKEN); - localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN); - } - }, - - isAuthenticated(): boolean { - if (!browser) return false; - return !!localStorage.getItem(STORAGE_KEYS.APP_TOKEN); - }, -}; - -// Decks API -export const decksApi = { - async getAll(): Promise { - const response = await fetchWithAuth(`${API_URL}/decks`); - if (!response.ok) throw new Error('Failed to fetch decks'); - return response.json(); - }, - - async getOne(id: string): Promise<{ deck: Deck; slides: Slide[] }> { - const response = await fetchWithAuth(`${API_URL}/decks/${id}`); - if (!response.ok) throw new Error('Failed to fetch deck'); - return response.json(); - }, - - async create(dto: CreateDeckDto): Promise { - const response = await fetchWithAuth(`${API_URL}/decks`, { - method: 'POST', - body: JSON.stringify(dto), - }); - if (!response.ok) throw new Error('Failed to create deck'); - return response.json(); - }, - - async update(id: string, dto: UpdateDeckDto): Promise { - const response = await fetchWithAuth(`${API_URL}/decks/${id}`, { - method: 'PUT', - body: JSON.stringify(dto), - }); - if (!response.ok) throw new Error('Failed to update deck'); - return response.json(); - }, - - async delete(id: string): Promise { - const response = await fetchWithAuth(`${API_URL}/decks/${id}`, { - method: 'DELETE', - }); - if (!response.ok) throw new Error('Failed to delete deck'); - }, -}; - -// Slides API -export const slidesApi = { - async create(deckId: string, dto: CreateSlideDto): Promise { - const response = await fetchWithAuth(`${API_URL}/decks/${deckId}/slides`, { - method: 'POST', - body: JSON.stringify(dto), - }); - if (!response.ok) throw new Error('Failed to create slide'); - return response.json(); - }, - - async update(id: string, dto: UpdateSlideDto): Promise { - const response = await fetchWithAuth(`${API_URL}/slides/${id}`, { - method: 'PUT', - body: JSON.stringify(dto), - }); - if (!response.ok) throw new Error('Failed to update slide'); - return response.json(); - }, - - async delete(id: string): Promise { - const response = await fetchWithAuth(`${API_URL}/slides/${id}`, { - method: 'DELETE', - }); - if (!response.ok) throw new Error('Failed to delete slide'); - }, - - async reorder(dto: ReorderSlidesDto): Promise { - const response = await fetchWithAuth(`${API_URL}/slides/reorder`, { - method: 'PUT', - body: JSON.stringify(dto), - }); - if (!response.ok) throw new Error('Failed to reorder slides'); - }, -}; - // Share API export interface ShareLink { id: string; @@ -233,9 +47,9 @@ export interface ShareLink { } export const shareApi = { - // Public - no auth required + /** Public — view a shared deck by share code (no auth required). */ async getByCode(code: string): Promise<{ deck: any; slides: any[] }> { - const response = await fetch(`${API_URL}/share/${code}`); + const response = await fetch(`${getServerUrl()}/api/share/${code}`); if (!response.ok) { if (response.status === 404) { throw new Error('Shared deck not found or link has expired'); @@ -245,9 +59,9 @@ export const shareApi = { return response.json(); }, - // Authenticated endpoints + /** Create a share link for a deck. */ async createShare(deckId: string, expiresAt?: string): Promise { - const response = await fetchWithAuth(`${API_URL}/share/deck/${deckId}`, { + const response = await fetchWithAuth(`${getServerUrl()}/api/share/deck/${deckId}`, { method: 'POST', body: JSON.stringify({ expiresAt }), }); @@ -255,14 +69,16 @@ export const shareApi = { return response.json(); }, + /** List share links for a deck. */ async getSharesForDeck(deckId: string): Promise { - const response = await fetchWithAuth(`${API_URL}/share/deck/${deckId}/links`); + const response = await fetchWithAuth(`${getServerUrl()}/api/share/deck/${deckId}/links`); if (!response.ok) throw new Error('Failed to get share links'); return response.json(); }, + /** Delete a share link. */ async deleteShare(shareId: string): Promise { - const response = await fetchWithAuth(`${API_URL}/share/${shareId}`, { + const response = await fetchWithAuth(`${getServerUrl()}/api/share/${shareId}`, { method: 'DELETE', }); if (!response.ok) throw new Error('Failed to delete share link'); diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index a493cd576..d90075db0 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -223,7 +223,7 @@ services: CALENDAR_BACKEND_URL: http://calendar-backend:3032 CONTACTS_BACKEND_URL: http://contacts-backend:3034 PICTURE_BACKEND_URL: http://picture-backend:3040 - PRESI_BACKEND_URL: http://presi-backend:3036 + # PRESI_BACKEND_URL: removed — replaced by Hono server # ZITARE_BACKEND_URL: removed — migrated to local-first PHOTOS_BACKEND_URL: http://photos-backend:3039 # CLOCK_BACKEND_URL: removed — migrated to local-first @@ -688,32 +688,8 @@ services: retries: 3 start_period: 50s - presi-backend: - build: - context: . - dockerfile: apps/presi/apps/backend/Dockerfile - image: presi-backend:local - container_name: mana-app-presi-backend - restart: always - depends_on: - mana-auth: - condition: service_healthy - environment: - NODE_ENV: production - PORT: 3036 - DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/presi - MANA_CORE_AUTH_URL: http://mana-auth:3001 - CORS_ORIGINS: https://presi.mana.how,https://mana.how - ADMIN_SERVICE_KEY: ${MANA_CORE_SERVICE_KEY} - GLITCHTIP_DSN: http://24df6aad72b646ba9fb68e54b566ad3e@glitchtip:8020/14 - ports: - - "3036:3036" - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3036/api/v1/health"] - interval: 120s - timeout: 10s - retries: 3 - start_period: 55s + # presi-backend: REPLACED by lightweight Hono server (apps/presi/apps/server) + # TODO: Add presi-server container when Bun Docker image is ready manadeck-backend: build: @@ -1329,15 +1305,14 @@ services: container_name: mana-app-presi-web restart: always depends_on: - presi-backend: + mana-auth: condition: service_healthy environment: NODE_ENV: production PORT: 5016 - PUBLIC_BACKEND_URL: http://presi-backend:3036 PUBLIC_MANA_CORE_AUTH_URL: http://mana-auth:3001 - PUBLIC_BACKEND_URL_CLIENT: https://presi-api.mana.how PUBLIC_MANA_CORE_AUTH_URL_CLIENT: https://auth.mana.how + PUBLIC_SYNC_SERVER_URL: ws://mana-sync:3050 ports: - "5016:5016" healthcheck: diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml index f14fd5b52..f7073a640 100644 --- a/docker/prometheus/prometheus.yml +++ b/docker/prometheus/prometheus.yml @@ -93,12 +93,7 @@ scrape_configs: metrics_path: '/metrics' scrape_interval: 30s - # Presi Backend - - job_name: 'presi-backend' - static_configs: - - targets: ['presi-backend:3036'] - metrics_path: '/metrics' - scrape_interval: 30s + # Presi Backend: REMOVED — replaced by Hono server # Nutriphi Backend - job_name: 'nutriphi-backend' diff --git a/package.json b/package.json index 67a4369ef..ca7ccba70 100644 --- a/package.json +++ b/package.json @@ -200,12 +200,9 @@ "presi:dev": "turbo run dev --filter=presi...", "dev:presi:web": "pnpm --filter @presi/web dev", "dev:presi:landing": "pnpm --filter @presi/landing dev", - "dev:presi:backend": "pnpm --filter @presi/backend dev", - "dev:presi:app": "turbo run dev --filter=@presi/web --filter=@presi/backend", - "dev:presi:full": "./scripts/setup-databases.sh presi && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:presi:backend\" \"pnpm dev:presi:web\"", - "presi:db:push": "pnpm --filter @presi/backend db:push", - "presi:db:studio": "pnpm --filter @presi/backend db:studio", - "presi:db:seed": "pnpm --filter @presi/backend db:seed", + "dev:presi:server": "cd apps/presi/apps/server && bun run --watch src/index.ts", + "dev:presi:app": "pnpm dev:presi:web", + "dev:presi:full": "concurrently -n auth,sync,server,web -c blue,magenta,yellow,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:presi:server\" \"pnpm dev:presi:web\"", "storage:dev": "turbo run dev --filter=storage...", "dev:storage:web": "pnpm --filter @storage/web dev", "dev:storage:backend": "pnpm --filter @storage/backend dev", @@ -260,14 +257,9 @@ "cf:login": "npx wrangler login", "cf:projects:list": "npx wrangler pages project list", "cf:projects:create": "echo 'Creating Cloudflare Pages projects...' && npx wrangler pages project create chat-landing --production-branch=main && npx wrangler pages project create picture-landing --production-branch=main && npx wrangler pages project create manacore-landing --production-branch=main && npx wrangler pages project create manadeck-landing --production-branch=main && npx wrangler pages project create zitare-landing --production-branch=main", - "dev:search": "pnpm --filter @manacore/mana-search dev", - "dev:search:docker": "docker-compose -f services/mana-search/docker-compose.dev.yml up -d", - "dev:search:docker:down": "docker-compose -f services/mana-search/docker-compose.dev.yml down", - "dev:search:docker:logs": "docker-compose -f services/mana-search/docker-compose.dev.yml logs -f", - "dev:search:full": "docker-compose -f services/mana-search/docker-compose.dev.yml up -d && pnpm --filter @manacore/mana-search dev", - "search:docker:up": "docker-compose -f services/mana-search/docker-compose.yml up -d", - "search:docker:down": "docker-compose -f services/mana-search/docker-compose.yml down", - "search:docker:logs": "docker-compose -f services/mana-search/docker-compose.yml logs -f", + "dev:search": "cd services/mana-search-go && go run ./cmd/server", + "dev:crawler": "cd services/mana-crawler-go && go run ./cmd/server", + "dev:notify": "cd services/mana-notify-go && go run ./cmd/server", "questions:dev": "turbo run dev --filter=questions...", "dev:questions:backend": "pnpm --filter @questions/backend dev", "dev:questions:web": "pnpm --filter @questions/web dev", diff --git a/scripts/generate-env.mjs b/scripts/generate-env.mjs index 736ecc899..91d8af1cc 100644 --- a/scripts/generate-env.mjs +++ b/scripts/generate-env.mjs @@ -270,20 +270,7 @@ const APP_CONFIGS = [ }, }, - // Presi Backend (NestJS) - { - path: 'apps/presi/apps/backend/.env', - vars: { - NODE_ENV: () => 'development', - PORT: (env) => env.PRESI_BACKEND_PORT || '3008', - DATABASE_URL: (env) => env.PRESI_DATABASE_URL, - MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, - DEV_BYPASS_AUTH: () => 'true', - DEV_USER_ID: (env) => env.DEV_USER_ID || '00000000-0000-0000-0000-000000000000', - JWT_PUBLIC_KEY: (env) => env.JWT_PUBLIC_KEY, - CORS_ORIGINS: (env) => env.CORS_ORIGINS, - }, - }, + // Presi Backend: REMOVED — replaced by Hono server (apps/presi/apps/server) // Presi Web (SvelteKit) { diff --git a/scripts/mac-mini/ensure-containers-running.sh b/scripts/mac-mini/ensure-containers-running.sh index 264974d14..70414267c 100755 --- a/scripts/mac-mini/ensure-containers-running.sh +++ b/scripts/mac-mini/ensure-containers-running.sh @@ -159,7 +159,6 @@ for container in $ALL_PROBLEM_CONTAINERS; do mana-app-storage-web) SERVICE_NAME="storage-web" ;; mana-app-storage-backend) SERVICE_NAME="storage-backend" ;; mana-app-presi-web) SERVICE_NAME="presi-web" ;; - mana-app-presi-backend) SERVICE_NAME="presi-backend" ;; mana-app-nutriphi-web) SERVICE_NAME="nutriphi-web" ;; mana-app-nutriphi-backend) SERVICE_NAME="nutriphi-backend" ;; mana-app-skilltree-web) SERVICE_NAME="skilltree-web" ;; diff --git a/scripts/setup-databases.sh b/scripts/setup-databases.sh index ab8eec860..a59c45f86 100755 --- a/scripts/setup-databases.sh +++ b/scripts/setup-databases.sh @@ -164,7 +164,7 @@ setup_service() { ;; presi) create_db_if_not_exists "presi" - push_schema "@presi/backend" "presi" + # Schema managed by mana-sync (NestJS backend removed, Hono server for shares) ;; storage) create_db_if_not_exists "storage"