From 93132d76c0b991e01af30611c6b7941b5cd0b876 Mon Sep 17 00:00:00 2001 From: RevIQ Date: Fri, 9 Jan 2026 11:45:03 +0800 Subject: [PATCH] Add api-server and CLI applications - Create api-server with Bun.serve: - oRPC router with stub handlers for all procedures - Auth middleware placeholder - CORS configuration - Create CLI tool with stricli: - bootstrap command for initial superuser creation - Placeholder commands for auth, user, org management Co-Authored-By: Claude Opus 4.5 --- apps/api-server/package.json | 22 ++ apps/api-server/src/index.ts | 22 ++ apps/api-server/src/middleware/auth.ts | 14 + apps/api-server/src/procedures/.gitkeep | 0 apps/api-server/src/router.ts | 347 ++++++++++++++++++++++++ apps/api-server/tsconfig.json | 24 ++ apps/cli/package.json | 25 ++ apps/cli/src/bin/reviq.ts | 122 +++++++++ apps/cli/src/commands/auth.ts | 25 ++ apps/cli/src/commands/bootstrap.ts | 77 ++++++ apps/cli/src/commands/org.ts | 25 ++ apps/cli/src/commands/user.ts | 17 ++ apps/cli/src/context.ts | 6 + apps/cli/tsconfig.json | 24 ++ 14 files changed, 750 insertions(+) create mode 100644 apps/api-server/package.json create mode 100644 apps/api-server/src/index.ts create mode 100644 apps/api-server/src/middleware/auth.ts create mode 100644 apps/api-server/src/procedures/.gitkeep create mode 100644 apps/api-server/src/router.ts create mode 100644 apps/api-server/tsconfig.json create mode 100644 apps/cli/package.json create mode 100644 apps/cli/src/bin/reviq.ts create mode 100644 apps/cli/src/commands/auth.ts create mode 100644 apps/cli/src/commands/bootstrap.ts create mode 100644 apps/cli/src/commands/org.ts create mode 100644 apps/cli/src/commands/user.ts create mode 100644 apps/cli/src/context.ts create mode 100644 apps/cli/tsconfig.json diff --git a/apps/api-server/package.json b/apps/api-server/package.json new file mode 100644 index 0000000..de514f2 --- /dev/null +++ b/apps/api-server/package.json @@ -0,0 +1,22 @@ +{ + "name": "api-server", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --hot src/index.ts", + "build": "bun build src/index.ts --outdir dist", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "@orpc/server": "^1.13.2", + "@reviq/api-contract": "workspace:*", + "@reviq/db": "workspace:*" + }, + "devDependencies": { + "@types/bun": "latest", + "@macalinao/tsconfig": "catalog:", + "typescript": "catalog:" + } +} diff --git a/apps/api-server/src/index.ts b/apps/api-server/src/index.ts new file mode 100644 index 0000000..b786266 --- /dev/null +++ b/apps/api-server/src/index.ts @@ -0,0 +1,22 @@ +import { RPCHandler } from "@orpc/server/fetch"; +import { router } from "./router.js"; + +const handler = new RPCHandler(router); + +Bun.serve({ + port: process.env.PORT || 3001, + async fetch(request) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/v1/rpc")) { + const { response } = await handler.handle(request, { + prefix: "/api/v1/rpc", + }); + return response ?? new Response("Not Found", { status: 404 }); + } + + return new Response("Not Found", { status: 404 }); + }, +}); + +console.log("API server running on port", process.env.PORT || 3001); diff --git a/apps/api-server/src/middleware/auth.ts b/apps/api-server/src/middleware/auth.ts new file mode 100644 index 0000000..575214b --- /dev/null +++ b/apps/api-server/src/middleware/auth.ts @@ -0,0 +1,14 @@ +/** + * Authentication middleware for oRPC server + * + * This middleware will be used to: + * - Verify JWT tokens from Authorization header + * - Extract user context from valid tokens + * - Attach user information to request context + * + * TODO: Implement in Phase 2 + */ + +export const authMiddleware = async (): Promise => { + throw new Error("Auth middleware not implemented"); +}; diff --git a/apps/api-server/src/procedures/.gitkeep b/apps/api-server/src/procedures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api-server/src/router.ts b/apps/api-server/src/router.ts new file mode 100644 index 0000000..c608226 --- /dev/null +++ b/apps/api-server/src/router.ts @@ -0,0 +1,347 @@ +import { implement } from "@orpc/server"; +import { contract } from "@reviq/api-contract"; + +const os = implement(contract); + +// Auth procedures +const signup = os.auth.signup.handler(async () => { + throw new Error("Not implemented"); +}); + +const verifyEmail = os.auth.verifyEmail.handler(async () => { + throw new Error("Not implemented"); +}); + +const resendVerificationEmail = os.auth.resendVerificationEmail.handler( + async () => { + throw new Error("Not implemented"); + }, +); + +const createLoginRequest = os.auth.createLoginRequest.handler(async () => { + throw new Error("Not implemented"); +}); + +const loginPassword = os.auth.loginPassword.handler(async () => { + throw new Error("Not implemented"); +}); + +const loginPasswordConfirm = os.auth.loginPasswordConfirm.handler(async () => { + throw new Error("Not implemented"); +}); + +const loginIfRequestIsCompleted = os.auth.loginIfRequestIsCompleted.handler( + async () => { + throw new Error("Not implemented"); + }, +); + +const forgotPassword = os.auth.forgotPassword.handler(async () => { + throw new Error("Not implemented"); +}); + +const resetPassword = os.auth.resetPassword.handler(async () => { + throw new Error("Not implemented"); +}); + +const logout = os.auth.logout.handler(async () => { + throw new Error("Not implemented"); +}); + +// WebAuthn procedures +const createRegistrationOptions = + os.auth.webauthn.createRegistrationOptions.handler(async () => { + throw new Error("Not implemented"); + }); + +const verifyRegistration = os.auth.webauthn.verifyRegistration.handler( + async () => { + throw new Error("Not implemented"); + }, +); + +const createAuthenticationOptions = + os.auth.webauthn.createAuthenticationOptions.handler(async () => { + throw new Error("Not implemented"); + }); + +const verifyAuthentication = os.auth.webauthn.verifyAuthentication.handler( + async () => { + throw new Error("Not implemented"); + }, +); + +// Me procedures +const meGet = os.me.get.handler(async () => { + throw new Error("Not implemented"); +}); + +const setupProfile = os.me.setupProfile.handler(async () => { + throw new Error("Not implemented"); +}); + +const updateProfile = os.me.updateProfile.handler(async () => { + throw new Error("Not implemented"); +}); + +const meDelete = os.me.delete.handler(async () => { + throw new Error("Not implemented"); +}); + +const setPassword = os.me.setPassword.handler(async () => { + throw new Error("Not implemented"); +}); + +const listPasskeys = os.me.listPasskeys.handler(async () => { + throw new Error("Not implemented"); +}); + +const createPasskey = os.me.createPasskey.handler(async () => { + throw new Error("Not implemented"); +}); + +const renamePasskey = os.me.renamePasskey.handler(async () => { + throw new Error("Not implemented"); +}); + +const deletePasskey = os.me.deletePasskey.handler(async () => { + throw new Error("Not implemented"); +}); + +const listSessions = os.me.listSessions.handler(async () => { + throw new Error("Not implemented"); +}); + +const revokeSession = os.me.revokeSession.handler(async () => { + throw new Error("Not implemented"); +}); + +const revokeAllSessions = os.me.revokeAllSessions.handler(async () => { + throw new Error("Not implemented"); +}); + +const getDeviceInfo = os.me.getDeviceInfo.handler(async () => { + throw new Error("Not implemented"); +}); + +const trustDevice = os.me.trustDevice.handler(async () => { + throw new Error("Not implemented"); +}); + +const listTrustedDevices = os.me.listTrustedDevices.handler(async () => { + throw new Error("Not implemented"); +}); + +const untrustDevice = os.me.untrustDevice.handler(async () => { + throw new Error("Not implemented"); +}); + +const revokeAllTrustedDevices = os.me.revokeAllTrustedDevices.handler( + async () => { + throw new Error("Not implemented"); + }, +); + +// Orgs procedures +const orgsList = os.orgs.list.handler(async () => { + throw new Error("Not implemented"); +}); + +const orgsCreate = os.orgs.create.handler(async () => { + throw new Error("Not implemented"); +}); + +const orgsGet = os.orgs.get.handler(async () => { + throw new Error("Not implemented"); +}); + +const orgsUpdate = os.orgs.update.handler(async () => { + throw new Error("Not implemented"); +}); + +const orgsDelete = os.orgs.delete.handler(async () => { + throw new Error("Not implemented"); +}); + +const orgsLeave = os.orgs.leave.handler(async () => { + throw new Error("Not implemented"); +}); + +// Orgs members procedures +const membersList = os.orgs.members.list.handler(async () => { + throw new Error("Not implemented"); +}); + +const membersUpdateRole = os.orgs.members.updateRole.handler(async () => { + throw new Error("Not implemented"); +}); + +const membersRemove = os.orgs.members.remove.handler(async () => { + throw new Error("Not implemented"); +}); + +// Orgs invites procedures +const invitesList = os.orgs.invites.list.handler(async () => { + throw new Error("Not implemented"); +}); + +const invitesCreate = os.orgs.invites.create.handler(async () => { + throw new Error("Not implemented"); +}); + +const invitesCancel = os.orgs.invites.cancel.handler(async () => { + throw new Error("Not implemented"); +}); + +const invitesAccept = os.orgs.invites.accept.handler(async () => { + throw new Error("Not implemented"); +}); + +// Orgs sites procedures +const sitesList = os.orgs.sites.list.handler(async () => { + throw new Error("Not implemented"); +}); + +// Admin orgs procedures +const adminOrgsList = os.admin.orgs.list.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminOrgsGet = os.admin.orgs.get.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminOrgsCreate = os.admin.orgs.create.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminOrgsUpdate = os.admin.orgs.update.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminOrgsDelete = os.admin.orgs.delete.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminOrgsListSites = os.admin.orgs.listSites.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminOrgsAddSite = os.admin.orgs.addSite.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminOrgsRemoveSite = os.admin.orgs.removeSite.handler(async () => { + throw new Error("Not implemented"); +}); + +// Admin users procedures +const adminUsersList = os.admin.users.list.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminUsersGet = os.admin.users.get.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminUsersCreate = os.admin.users.create.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminUsersUpdate = os.admin.users.update.handler(async () => { + throw new Error("Not implemented"); +}); + +const adminUsersConfirmEmail = os.admin.users.confirmEmail.handler(async () => { + throw new Error("Not implemented"); +}); + +// Admin auth procedures +const adminAuthCompleteLogin = os.admin.auth.completeLogin.handler(async () => { + throw new Error("Not implemented"); +}); + +// Build the router +export const router = os.router({ + auth: { + signup, + verifyEmail, + resendVerificationEmail, + createLoginRequest, + loginPassword, + loginPasswordConfirm, + loginIfRequestIsCompleted, + forgotPassword, + resetPassword, + logout, + webauthn: { + createRegistrationOptions, + verifyRegistration, + createAuthenticationOptions, + verifyAuthentication, + }, + }, + me: { + get: meGet, + setupProfile, + updateProfile, + delete: meDelete, + setPassword, + listPasskeys, + createPasskey, + renamePasskey, + deletePasskey, + listSessions, + revokeSession, + revokeAllSessions, + getDeviceInfo, + trustDevice, + listTrustedDevices, + untrustDevice, + revokeAllTrustedDevices, + }, + orgs: { + list: orgsList, + create: orgsCreate, + get: orgsGet, + update: orgsUpdate, + delete: orgsDelete, + leave: orgsLeave, + members: { + list: membersList, + updateRole: membersUpdateRole, + remove: membersRemove, + }, + invites: { + list: invitesList, + create: invitesCreate, + cancel: invitesCancel, + accept: invitesAccept, + }, + sites: { + list: sitesList, + }, + }, + admin: { + orgs: { + list: adminOrgsList, + get: adminOrgsGet, + create: adminOrgsCreate, + update: adminOrgsUpdate, + delete: adminOrgsDelete, + listSites: adminOrgsListSites, + addSite: adminOrgsAddSite, + removeSite: adminOrgsRemoveSite, + }, + users: { + list: adminUsersList, + get: adminUsersGet, + create: adminUsersCreate, + update: adminUsersUpdate, + confirmEmail: adminUsersConfirmEmail, + }, + auth: { + completeLogin: adminAuthCompleteLogin, + }, + }, +}); diff --git a/apps/api-server/tsconfig.json b/apps/api-server/tsconfig.json new file mode 100644 index 0000000..e024e7f --- /dev/null +++ b/apps/api-server/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "@macalinao/tsconfig/tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["@types/bun"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedDeclarations": false, + "composite": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 0000000..7ba394b --- /dev/null +++ b/apps/cli/package.json @@ -0,0 +1,25 @@ +{ + "name": "@reviq/cli", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "reviq": "./dist/index.js" + }, + "scripts": { + "build": "bun build src/bin/reviq.ts --outdir dist --target bun", + "cli": "bun run src/bin/reviq.ts", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "@stricli/core": "^1.2.5", + "@reviq/db": "workspace:*", + "@noble/hashes": "^2.0.1" + }, + "devDependencies": { + "@types/bun": "latest", + "@macalinao/tsconfig": "catalog:", + "typescript": "catalog:" + } +} diff --git a/apps/cli/src/bin/reviq.ts b/apps/cli/src/bin/reviq.ts new file mode 100644 index 0000000..734359f --- /dev/null +++ b/apps/cli/src/bin/reviq.ts @@ -0,0 +1,122 @@ +#!/usr/bin/env bun + +import { + buildApplication, + buildCommand, + buildRouteMap, + run, +} from "@stricli/core"; + +// Lazy load command implementations +const bootstrap = buildCommand({ + loader: async () => import("../commands/bootstrap.js"), + parameters: {}, + docs: { + brief: "Create a superuser account", + }, +}); + +const authLogin = buildCommand({ + loader: async () => import("../commands/auth.js").then((m) => m.login), + parameters: {}, + docs: { brief: "Login to RevIQ (stub)" }, +}); + +const authLogout = buildCommand({ + loader: async () => import("../commands/auth.js").then((m) => m.logout), + parameters: {}, + docs: { brief: "Logout from RevIQ (stub)" }, +}); + +const authStatus = buildCommand({ + loader: async () => import("../commands/auth.js").then((m) => m.status), + parameters: {}, + docs: { brief: "Check authentication status (stub)" }, +}); + +const authCommand = buildRouteMap({ + routes: { + login: authLogin, + logout: authLogout, + status: authStatus, + }, + docs: { + brief: "Authentication commands", + }, +}); + +const userCreate = buildCommand({ + loader: async () => import("../commands/user.js").then((m) => m.create), + parameters: {}, + docs: { brief: "Create a new user (stub)" }, +}); + +const userConfirmEmail = buildCommand({ + loader: async () => import("../commands/user.js").then((m) => m.confirmEmail), + parameters: {}, + docs: { brief: "Confirm user email (stub)" }, +}); + +const userCommand = buildRouteMap({ + routes: { + create: userCreate, + "confirm-email": userConfirmEmail, + }, + docs: { + brief: "User management commands", + }, +}); + +const orgCreate = buildCommand({ + loader: async () => import("../commands/org.js").then((m) => m.create), + parameters: {}, + docs: { brief: "Create an organization (stub)" }, +}); + +const orgList = buildCommand({ + loader: async () => import("../commands/org.js").then((m) => m.list), + parameters: {}, + docs: { brief: "List organizations (stub)" }, +}); + +const orgAddSite = buildCommand({ + loader: async () => import("../commands/org.js").then((m) => m.addSite), + parameters: {}, + docs: { brief: "Add a site to an organization (stub)" }, +}); + +const orgCommand = buildRouteMap({ + routes: { + create: orgCreate, + list: orgList, + "add-site": orgAddSite, + }, + docs: { + brief: "Organization management commands", + }, +}); + +const rootMap = buildRouteMap({ + routes: { + bootstrap, + auth: authCommand, + user: userCommand, + org: orgCommand, + }, + docs: { + brief: "RevIQ CLI for database and user management", + }, +}); + +const app = buildApplication(rootMap, { + name: "reviq", + versionInfo: { + currentVersion: "0.0.0", + }, +}); + +const context = { + process, +}; + +await run(app, process.argv.slice(2), context); diff --git a/apps/cli/src/commands/auth.ts b/apps/cli/src/commands/auth.ts new file mode 100644 index 0000000..6881213 --- /dev/null +++ b/apps/cli/src/commands/auth.ts @@ -0,0 +1,25 @@ +import type { CommandContext } from "@stricli/core"; + +/** + * Login command stub + */ +export async function login(this: CommandContext): Promise { + console.log("Auth login command - Not implemented"); + console.log("This command will authenticate a user and store credentials"); +} + +/** + * Logout command stub + */ +export async function logout(this: CommandContext): Promise { + console.log("Auth logout command - Not implemented"); + console.log("This command will clear stored authentication credentials"); +} + +/** + * Status command stub + */ +export async function status(this: CommandContext): Promise { + console.log("Auth status command - Not implemented"); + console.log("This command will show current authentication status"); +} diff --git a/apps/cli/src/commands/bootstrap.ts b/apps/cli/src/commands/bootstrap.ts new file mode 100644 index 0000000..eb3f19f --- /dev/null +++ b/apps/cli/src/commands/bootstrap.ts @@ -0,0 +1,77 @@ +import type { CommandContext } from "@stricli/core"; +// Password hashing imports (for future implementation) +// import { scrypt } from "@noble/hashes/scrypt"; +// import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils"; + +/** + * Bootstrap command - creates a superuser account + * + * This command should be run after dbmate migration to set up + * the initial superuser account. + * + * Uses scrypt for password hashing (Cloudflare Workers compatible via @noble/hashes) + */ +export default async function (this: CommandContext): Promise { + console.log("RevIQ Bootstrap - Create Superuser"); + console.log("===================================\n"); + + // In a real implementation, we would: + // 1. Prompt for email and password using readline or prompts + // 2. Validate the input + // 3. Hash the password with scrypt (via @noble/hashes) + // 4. Connect to the database using @reviq/db + // 5. Insert the user with is_superuser=true + // 6. Handle errors appropriately + + console.log("TODO: Implement bootstrap command"); + console.log("\nThis command will:"); + console.log(" 1. Prompt for email address"); + console.log(" 2. Prompt for password (with confirmation)"); + console.log(" 3. Hash password using scrypt (@noble/hashes)"); + console.log(" 4. Create user in database with is_superuser=true"); + console.log("\nRequirements:"); + console.log(" - Database must be migrated (run 'dbmate up' first)"); + console.log(" - DATABASE_URL environment variable must be set"); + + // Example of what the implementation would look like: + /* + import readline from 'readline'; + import { db } from '@reviq/db'; + import { randomBytes } from 'crypto'; + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const email = await new Promise((resolve) => { + rl.question('Email: ', resolve); + }); + + const password = await new Promise((resolve) => { + rl.question('Password: ', resolve); + }); + + // Generate a random salt + const salt = randomBytes(16); + + // Hash with scrypt using recommended parameters + // N=2^14 (16384), r=8, p=1 - good balance of security and performance + const hash = scrypt(utf8ToBytes(password), salt, { N: 16384, r: 8, p: 1, dkLen: 32 }); + + // Store as: $scrypt$N=16384,r=8,p=1$$ + const hashedPassword = `$scrypt$N=16384,r=8,p=1$${bytesToHex(salt)}$${bytesToHex(hash)}`; + + await db.insertInto('users') + .values({ + email: email.toLowerCase(), + password_hash: hashedPassword, + is_superuser: true, + email_verified_at: new Date(), + }) + .execute(); + + console.log('Superuser created successfully!'); + rl.close(); + */ +} diff --git a/apps/cli/src/commands/org.ts b/apps/cli/src/commands/org.ts new file mode 100644 index 0000000..5f7fea5 --- /dev/null +++ b/apps/cli/src/commands/org.ts @@ -0,0 +1,25 @@ +import type { CommandContext } from "@stricli/core"; + +/** + * Create organization command stub + */ +export async function create(this: CommandContext): Promise { + console.log("Org create command - Not implemented"); + console.log("This command will create a new organization"); +} + +/** + * List organizations command stub + */ +export async function list(this: CommandContext): Promise { + console.log("Org list command - Not implemented"); + console.log("This command will list all organizations"); +} + +/** + * Add site to organization command stub + */ +export async function addSite(this: CommandContext): Promise { + console.log("Org add-site command - Not implemented"); + console.log("This command will add a site to an organization"); +} diff --git a/apps/cli/src/commands/user.ts b/apps/cli/src/commands/user.ts new file mode 100644 index 0000000..8c0768c --- /dev/null +++ b/apps/cli/src/commands/user.ts @@ -0,0 +1,17 @@ +import type { CommandContext } from "@stricli/core"; + +/** + * Create user command stub + */ +export async function create(this: CommandContext): Promise { + console.log("User create command - Not implemented"); + console.log("This command will create a new user account"); +} + +/** + * Confirm email command stub + */ +export async function confirmEmail(this: CommandContext): Promise { + console.log("User confirm-email command - Not implemented"); + console.log("This command will confirm a user's email address"); +} diff --git a/apps/cli/src/context.ts b/apps/cli/src/context.ts new file mode 100644 index 0000000..ab2ece8 --- /dev/null +++ b/apps/cli/src/context.ts @@ -0,0 +1,6 @@ +/** + * Local context for CLI application + */ +export interface LocalContext { + readonly process: NodeJS.Process; +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json new file mode 100644 index 0000000..e024e7f --- /dev/null +++ b/apps/cli/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "@macalinao/tsconfig/tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["@types/bun"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedDeclarations": false, + "composite": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}