mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-28 18:57:42 +02:00
refactor: consolidate codebase — remove archived code, deduplicate packages, standardize middleware
- Delete 17 server-archived/ directories (consolidated into apps/api/) - Delete apps-archived/ (clock, wisekeep) and services-archived/ (it-landing, ollama-metrics-proxy) - Fix type safety: replace all `any` casts in cross-app-queries.ts with proper Local* types - Remove duplicate shared-auth-stores package (identical copy of shared-auth-ui/stores/) - Remove duplicate theme store from shared-stores (canonical version in shared-theme) - Migrate memoro-server rate-limiter to shared-hono/rateLimitMiddleware - Migrate uload-server JWT auth + error handler to shared-hono (authMiddleware, errorHandler) - Migrate arcade-server error handling to shared-hono - Merge shared-profile-ui and shared-app-onboarding into shared-ui - Unify /clock route into /times/clock, remove redirect stubs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7ee57b7afd
commit
d8ce4eaf34
309 changed files with 172 additions and 21667 deletions
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
/**
|
||||
* 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 {
|
||||
pgSchema,
|
||||
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/mana_platform';
|
||||
|
||||
const connection = postgres(DATABASE_URL, {
|
||||
max: 5,
|
||||
idle_timeout: 20,
|
||||
});
|
||||
|
||||
// ─── Schema (read-only for share lookups) ────────────────
|
||||
|
||||
export const presiSchema = pgSchema('presi');
|
||||
|
||||
export const decks = presiSchema.table('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 = presiSchema.table(
|
||||
'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 = presiSchema.table('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 = presiSchema.table(
|
||||
'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;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/**
|
||||
* 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,
|
||||
};
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
/**
|
||||
* 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 };
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["bun"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue