diff --git a/docs/architecture/security.md b/docs/architecture/security.md index 9d5813267..3bfd5527c 100644 --- a/docs/architecture/security.md +++ b/docs/architecture/security.md @@ -61,6 +61,12 @@ const verified = scram(password, storedHash); - Tokens expire after inactivity - Tokens can be revoked immediately +### Login Lockout & Backoff + +- Failed login attempts are tracked per username and rate-limited. +- After `MB_AUTH_LOCKOUT_MAX_ATTEMPTS` within `MB_AUTH_LOCKOUT_WINDOW_MS`, logins lock for `MB_AUTH_LOCKOUT_MS`. +- When locked, the API returns `429` with `Retry-After` to guide clients. + ## 🏢 Multi-Tenant Isolation ### Tenant Boundaries @@ -257,8 +263,7 @@ const hashedKey = sha512(API_KEY); // Hash for storage ## 🔗 Related Documentation -TODO: Security guidelines live at ../security/SECURITY.md; update this link. -- [Security Guidelines](../SECURITY.md) - Security policy +- [Security Guidelines](../security/SECURITY.md) - Security policy - [5-Level System](./5-level-system.md) - Permission model - [Database Architecture](./database.md) - Data storage - [API Development](../guides/api-development.md) - API security diff --git a/frontends/nextjs/src/app/api/packages/data/[packageId]/route.ts b/frontends/nextjs/src/app/api/packages/data/[packageId]/route.ts new file mode 100644 index 000000000..660ae1d83 --- /dev/null +++ b/frontends/nextjs/src/app/api/packages/data/[packageId]/route.ts @@ -0,0 +1,3 @@ +export { GET } from './handlers/get-package-data' +export { PUT } from './handlers/put-package-data' +export { DELETE } from './handlers/delete-package-data' diff --git a/frontends/nextjs/src/app/api/packages/installed/[packageId]/handlers/delete-installed-package.ts b/frontends/nextjs/src/app/api/packages/installed/[packageId]/handlers/delete-installed-package.ts new file mode 100644 index 000000000..dc490752a --- /dev/null +++ b/frontends/nextjs/src/app/api/packages/installed/[packageId]/handlers/delete-installed-package.ts @@ -0,0 +1,40 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { getInstalledPackages } from '@/lib/db/packages/get-installed-packages' +import { uninstallPackage } from '@/lib/db/packages/uninstall-package' +import { getPackageCatalogEntry } from '@/lib/packages/server/get-package-catalog-entry' +import { uninstallPackageContent } from '@/lib/packages/server/uninstall-package-content' + +interface RouteParams { + params: { + packageId: string + } +} + +export async function DELETE(_request: NextRequest, { params }: RouteParams) { + try { + const entry = getPackageCatalogEntry(params.packageId) + if (!entry) { + return NextResponse.json({ error: 'Package not found' }, { status: 404 }) + } + + const installed = await getInstalledPackages() + if (!installed.some((pkg) => pkg.packageId === params.packageId)) { + return NextResponse.json({ error: 'Package not installed' }, { status: 404 }) + } + + await uninstallPackageContent(params.packageId, entry.content) + await uninstallPackage(params.packageId) + + return NextResponse.json({ deleted: true }) + } catch (error) { + console.error('Error uninstalling package:', error) + return NextResponse.json( + { + error: 'Failed to uninstall package', + details: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ) + } +} diff --git a/frontends/nextjs/src/lib/db/users/get-super-god.ts b/frontends/nextjs/src/lib/db/users/get-super-god.ts index 19ce7f005..c2bf80a7e 100644 --- a/frontends/nextjs/src/lib/db/users/get-super-god.ts +++ b/frontends/nextjs/src/lib/db/users/get-super-god.ts @@ -1,5 +1,6 @@ import { getAdapter } from '../dbal-client' import type { User } from '../../types/level-types' +import { mapUserRecord } from './map-user-record' /** * Get the SuperGod user (instance owner) @@ -9,17 +10,5 @@ export async function getSuperGod(): Promise { const result = await adapter.list('User', { filter: { isInstanceOwner: true } }) if (result.data.length === 0) return null - const user = result.data[0] as any - - return { - id: user.id, - username: user.username, - email: user.email, - role: user.role as any, - profilePicture: user.profilePicture || undefined, - bio: user.bio || undefined, - createdAt: Number(user.createdAt), - tenantId: user.tenantId || undefined, - isInstanceOwner: user.isInstanceOwner, - } + return mapUserRecord(result.data[0] as Record) } diff --git a/frontends/nextjs/src/lib/db/users/get-users.ts b/frontends/nextjs/src/lib/db/users/get-users.ts index 89dff8e00..e4548a148 100644 --- a/frontends/nextjs/src/lib/db/users/get-users.ts +++ b/frontends/nextjs/src/lib/db/users/get-users.ts @@ -1,5 +1,6 @@ import { getAdapter } from '../dbal-client' import type { User } from '../../types/level-types' +import { mapUserRecord } from './map-user-record' export type GetUsersOptions = | { tenantId: string } @@ -13,15 +14,5 @@ export async function getUsers(options: GetUsersOptions): Promise { const adapter = getAdapter() const listOptions = 'tenantId' in options ? { filter: { tenantId: options.tenantId } } : undefined const result = listOptions ? await adapter.list('User', listOptions) : await adapter.list('User') - return (result.data as any[]).map((u) => ({ - id: u.id, - username: u.username, - email: u.email, - role: u.role as any, - profilePicture: u.profilePicture || undefined, - bio: u.bio || undefined, - createdAt: Number(u.createdAt), - tenantId: u.tenantId || undefined, - isInstanceOwner: u.isInstanceOwner, - })) + return (result.data as Record[]).map((user) => mapUserRecord(user)) }