From db9b72292911751a4fee72b9100df6db8a8cab31 Mon Sep 17 00:00:00 2001 From: Jackson Roberts Date: Tue, 24 Feb 2026 18:30:45 -0600 Subject: [PATCH] release workflow --- .docker/docker-compose.yml | 6 +- .github/copilot-instructions.md | 258 +++++++++++++----- .github/workflows/build-and-publish.yaml | 63 +++++ API_ROUTES.md | 4 +- Dockerfile | 57 ++++ PERMISSIONS.md | 2 +- .../Check User Permission.bru | 0 .../{ttscm => optima}/Fetch Company Pages.bru | 0 bruno/{ttscm => optima}/Teapot.bru | 0 bruno/optima/bruno.json | 9 + bruno/ttscm/bruno.json | 9 +- kubernetes/deployment.yaml | 26 ++ kubernetes/ingress.yaml | 36 +++ package.json | 1 + utils/genProductionKeys.ts | 4 +- 15 files changed, 398 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/build-and-publish.yaml create mode 100644 Dockerfile rename bruno/{ttscm => optima}/Check User Permission.bru (100%) rename bruno/{ttscm => optima}/Fetch Company Pages.bru (100%) rename bruno/{ttscm => optima}/Teapot.bru (100%) create mode 100644 bruno/optima/bruno.json create mode 100644 kubernetes/deployment.yaml create mode 100644 kubernetes/ingress.yaml diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml index 541011b..fe2be7c 100644 --- a/.docker/docker-compose.yml +++ b/.docker/docker-compose.yml @@ -5,9 +5,9 @@ services: image: postgres:17 restart: unless-stopped environment: - POSTGRES_USER: ttscm + POSTGRES_USER: optima POSTGRES_PASSWORD: 123web123 - POSTGRES_DB: ttscm + POSTGRES_DB: optima volumes: - ./postgres:/var/lib/postgresql/data ports: @@ -34,4 +34,4 @@ services: container_name: redisinsight ports: - "5540:5540" - restart: unless-stopped \ No newline at end of file + restart: unless-stopped diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 745f1fc..2c1bd0b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,89 +1,221 @@ -# Copilot / AI Agent Instructions for ttscm-api +# Copilot / AI Agent Instructions for optima-api Purpose: make AI coding agents immediately productive in this repository by describing architecture, conventions, workflows, and helpful code pointers. --- **Big picture**: This is a TypeScript API service (runs on Bun) using the Hono framework. The HTTP surface is implemented in `src/api` where small router files are mounted on a versioned router in `src/api/server.ts` (see the `/v1` mount). Typical request flow is: +--- -- `router` (in `src/api/routers/*`) → `controller` (in `src/controllers/*`) → `manager` (in `src/managers/*`) → `module` / `generated/prisma` for persistence and external integrations. -- Keep each layer focused: routers handle routing & middleware, controllers handle request validation and high-level orchestration, managers encapsulate domain/persistence logic, modules provide shared utilities (external API clients, helpers). +## Big picture -- **Runtime / tooling**: The project runs on Bun. Dev command: `npm run dev` (runs `bun --watch src/index.ts`). DB tooling uses Prisma; generated client lives under `generated/prisma` (do NOT edit generated files). Key scripts in `package.json`: - - `dev` — start the server with Bun in watch mode - - `db:gen` — `prisma generate` - - `db:push` — `prisma migrate dev --skip-generate` +This is a TypeScript API service (runs on **Bun**) using the **Hono** framework. The HTTP surface is implemented in `src/api` where small route-handler files are mounted on a versioned router in `src/api/server.ts` (see the `/v1` mount). The typical request flow is: -- **Data layer**: Prisma schema is at `prisma/schema.prisma`. The app imports the generated Prisma client from `generated/prisma/client.ts` (or `generated/prisma/browser.ts` in browser contexts). Always run the `db:gen` script after updating `schema.prisma`. +``` +server.ts → routers/Router.ts → api//*.ts (route handlers) → managers/.ts → controllers (domain models) / modules / generated/prisma +``` -- **Routing & controllers**: Example flow: a request hits a `router` in `src/api/routers/*` → router delegates to a `controller` in `src/controllers/*` → controller calls a `manager` in `src/managers/*` for domain/persistence logic. Important concrete patterns: - - `src/api/server.ts` mounts `v1` and uses `v1.route("/auth", require("./routers/authRouter").default)` style requires; preserve this shape when adding routes. - - Router files export a default Hono router object (CommonJS `module.exports`/`export default` mixture is used across the codebase). - - Controllers are single-export modules named like `CompanyController.ts` with named methods per action (e.g., `fetch`, `update`). Prefer small methods that call into `managers/*`. - - Managers are thin domain layers (e.g., `managers/companies.ts`) that wrap `generated/prisma` calls and other I/O. Keep side effects here, keep controllers pure orchestration. - - Use `src/modules/api-utils/apiResponse.ts` for every HTTP response shape (successful/created/error/internalError/zodError). - - Use Zod schemas in controllers for request validation; server-level `app.onError` maps `ZodError` to `apiResponse.zodError`. +Keep each layer focused: -**API layout & conventions (how to add a new endpoint)** +- **Route handler files** (`src/api//*.ts`) — define individual endpoints using the `createRoute()` utility, handle request validation with Zod, call managers, and return `apiResponse.*` results. +- **Routers** (`src/api/routers/*`) — aggregate route handler files from a domain's `index.ts` and re-mount them under a single prefix. Mounted in `src/api/server.ts`. +- **Managers** (`src/managers/*`) — thin domain/persistence layers that wrap `generated/prisma` calls and other I/O. Managers instantiate and return **controller** instances as domain objects. +- **Controllers** (`src/controllers/*`) — **domain model classes** (e.g., `CompanyController`, `CredentialController`, `UserController`) that encapsulate entity state and domain methods. They are NOT request handlers. Managers create and return controller instances. +- **Modules** (`src/modules/*`) — shared utilities, external API clients (ConnectWise, UniFi, Microsoft), credential helpers, permission utilities, and tools. -- **Add router**: create `src/api/routers/Router.ts` exporting a Hono router and mount it in `src/api/server.ts` under the `v1` router. -- **Add controller**: create `src/controllers/Controller.ts` exporting functions for each action. Controllers should validate input with Zod, call managers, and return `apiResponse.*` results. -- **Add manager**: create `src/managers/.ts` for persistence/domain logic. Use the generated Prisma client (`generated/prisma/client.ts`) here; do not import Prisma directly in controllers. -- **Add modules/types**: if needed, add helpers to `src/modules/*` and runtime types to `src/types/*`. -- **Middleware & auth**: use `src/api/middleware/authorization.ts` for protecting routes; follow existing token/session patterns from `src/controllers/SessionController.ts` and `src/Errors/*`. -- **Error handling**: throw repository-specific errors from `src/Errors/*` (include `status`, `name`, `message`, optional `cause`) and let `src/api/server.ts` map them via `apiResponse.error`. -- **Naming**: prefer plural manager filenames (`companies.ts`) and singular controller names (`CompanyController.ts`) — follow existing files. +--- -**Examples & notable files** +## Runtime / tooling + +The project runs on **Bun**. DB tooling uses **Prisma**; the generated client lives under `generated/prisma` (do NOT edit generated files). Key scripts in `package.json`: + +- `dev` — `NODE_ENV=development bun --watch src/index.ts` (start dev server with hot reload) +- `db:gen` — `prisma generate` +- `db:push` — `prisma migrate dev --skip-generate` +- `utils:dev` — `docker compose -f .docker/docker-compose.yml up --build` +- `utils:gen_private_keys` — `bun ./utils/genPrivateKeys` +- `utils:create_admin_role` — `bun ./utils/createAdminRole` +- `utils:assign_user_role` — `bun ./utils/assignUserRole` + +## Data layer + +Prisma schema is at `prisma/schema.prisma`. The app imports the generated Prisma client from `generated/prisma/client.ts` (or `generated/prisma/browser.ts` for browser type contexts). The shared `prisma` instance is exported from `src/constants.ts`. Always run `npm run db:gen` after updating `schema.prisma`. + +## Shared constants (`src/constants.ts`) + +This file exports critical shared instances used across the codebase: + +- `prisma` — the PrismaClient instance (via `@prisma/adapter-pg`) +- `PORT`, session/token durations, private/public keys for JWT signing +- `msalClient` — Microsoft MSAL client for OAuth +- `connectWiseApi` — Axios instance for ConnectWise API +- `unifi` — `UnifiClient` instance for UniFi controller interaction + +--- + +## Route handler pattern (`createRoute`) + +Every route handler file uses the `createRoute()` utility from `src/modules/api-utils/createRoute.ts`. It creates a self-contained Hono sub-app for a single endpoint: + +```ts +export default createRoute( + "get", // HTTP method + ["/companies"], // path(s) + async (c) => { + /* handler */ + }, // request handler + authMiddleware({ permissions: ["company.fetch.many"] }), // middleware (spread) +); +``` + +Route files live in `src/api//*.ts`. Each domain folder has an `index.ts` that re-exports all route modules. Router files (`src/api/routers/Router.ts`) import from the domain's index and auto-mount all routes: + +```ts +import * as companyRoutes from "../companies"; +const companyRouter = new Hono(); +Object.values(companyRoutes).map((r) => companyRouter.route("/", r)); +export default companyRouter; +``` + +`src/api/server.ts` then mounts each router under `/v1`: + +```ts +v1.route("/company", require("./routers/companyRouter").default); +``` + +## Routing & domain organization + +The `server.ts` file mounts these routers under `/v1`: + +- `/teapot` — health check +- `/auth` — Microsoft OAuth flow (`src/api/auth/*`) +- `/user` — user routes (`src/api/user/*`) including `@me` sub-routes +- `/company` — company routes (`src/api/companies/*`) +- `/credential` — credential routes (`src/api/credentials/*`) +- `/credential-type` — credential type routes (`src/api/credential-types/*`) +- `/role` — role management (`src/api/roles/*`) +- `/permissions` — permission node queries (`src/api/permissions/*`) +- `/unifi` — UniFi integration (`src/api/unifi/*` with `sites/` and `site/` sub-folders) + +--- + +## API layout & conventions (how to add a new endpoint) + +1. **Add route handler files**: Create `src/api//.ts` files, each exporting a default `createRoute(...)` call. Validate input with Zod inside the handler, call managers for business logic, and return responses via `apiResponse.*`. +2. **Add domain index**: Create `src/api//index.ts` that re-exports all route modules from the folder. +3. **Add router**: Create `src/api/routers/Router.ts` that imports all routes from the domain's index and mounts them. Mount it in `src/api/server.ts` under `v1`. +4. **Add manager**: Create `src/managers/.ts` (plural filename) for persistence/domain logic. Use the `prisma` instance from `src/constants.ts`; do not import Prisma directly in route handlers. Managers should instantiate and return controller instances when the domain warrants it. +5. **Add controller** (if needed): Create `src/controllers/Controller.ts` (singular filename) as a **class** that encapsulates entity state, domain methods, and a `toJson()` serializer. Controllers are domain model objects — they do NOT handle HTTP requests. +6. **Add modules/types**: If needed, add helpers to `src/modules/*` and runtime types to `src/types/*`. +7. **Middleware & auth**: Use `authMiddleware()` from `src/api/middleware/authorization.ts` as the last argument to `createRoute()`. It accepts `{ permissions?: string[], scopes?: string[], forbiddenAuthTypes?: string[] }`. Follow existing token/session patterns from `src/controllers/SessionController.ts` and `src/Errors/*`. +8. **Error handling**: Throw repository-specific errors from `src/Errors/*` (include `status`, `name`, `message`, optional `cause`) and let `src/api/server.ts` map them via `apiResponse.error`. +9. **Naming conventions**: plural manager filenames (`companies.ts`), singular controller class names (`CompanyController.ts`), descriptive route handler filenames (`fetchAll.ts`, `create.ts`, `update.ts`). + +--- + +## Examples & notable files - `src/api/server.ts` — mounts `v1`, registers `cors`, central `onError` handler and `notFound` response. -- `src/api/routers/companyRouter.ts` and `src/controllers/CompanyController.ts` — canonical example for adding company endpoints. -- `src/api/user/@me/*` — nested route example (use Hono sub-routers for subpaths). -- `src/modules/cw-utils/*` — external API integrations; keep interfaces stable and return domain objects consumed by managers. +- `src/api/companies/fetchAll.ts` — canonical example of a route handler file using `createRoute()`, `authMiddleware()`, managers, and `apiResponse`. +- `src/api/credentials/create.ts` — example of Zod validation inside a route handler. +- `src/api/routers/companyRouter.ts` — canonical router that auto-mounts all route modules from a domain folder. +- `src/api/user/@me/*` — nested sub-route example (user's own profile endpoints). +- `src/controllers/CompanyController.ts` — example domain model class with methods like `refreshFromCW()`, `fetchCwData()`, and `toJson()`. +- `src/controllers/CredentialController.ts` — example of a richer domain model class with field validation, secure value handling, and sub-credential support. +- `src/managers/companies.ts` — example manager calling Prisma and returning `CompanyController` instances. +- `src/modules/api-utils/createRoute.ts` — the `createRoute()` utility used by every route handler. +- `src/modules/cw-utils/*` — external ConnectWise API integrations; keep interfaces stable and return domain objects consumed by managers. +- `src/modules/unifi-api/UnifiClient.ts` — UniFi controller API client class with methods for sites, WLANs, devices, networks, etc. +- `src/modules/credentials/*` — credential field validation, secure value encryption/decryption, and type definitions. +- `src/constants.ts` — shared instances (`prisma`, API clients, keys, durations). -- **Validation & errors**: Zod is used for input validation; Zod errors are handled centrally in `src/api/server.ts` via `apiResponse.zodError`. Application errors use custom error classes in `src/Errors/*` (e.g., `AuthenticationError.ts`, `GenericError.ts`). When creating errors, follow the shape used in existing errors (include `status`, `name`, `message`, and optional `cause`). +--- -- **Response pattern**: Use the `apiResponse` helpers in `src/modules/api-utils/apiResponse.ts` for formatting responses and status codes (successful, created, error, internalError, zodError). +## Validation & errors -- **Auth & external integrations**: Microsoft OAuth flow is under `src/api/auth/*` and `src/modules/fetchMicrosoftUser.ts`. If touching authentication, follow existing redirect/refresh patterns in `src/api/auth` and preserve token-refresh semantics. +Zod is used for input validation **inside route handler files** (not controllers). Zod errors are handled centrally in `src/api/server.ts` via `apiResponse.zodError`. Application errors use custom error classes in `src/Errors/*` (e.g., `AuthenticationError.ts`, `GenericError.ts`, `AuthorizationError.ts`, `InsufficientPermission.ts`). When creating errors, follow the shape used in existing errors (include `status`, `name`, `message`, and optional `cause`). -- **ConnectWise integration**: Utilities for ConnectWise interactions live in `src/modules/cw-utils/*` (e.g., `fetchCompanyConfigurations.ts`). These modules often call external APIs and return domain data; preserve the module contracts (input types and returned shapes) when refactoring. +## Response pattern -- **Generated files and CI**: `generated/` is a build artifact. Do not modify. When updating Prisma models, run `npm run db:gen` and commit changes to `generated/prisma` only if that's the established workflow. +Use the `apiResponse` helpers in `src/modules/api-utils/apiResponse.ts` for formatting all HTTP responses: -- **Files to inspect for context / examples**: - - `src/api/server.ts` — central error handling, router mounting, CORS and not-found handling. - - `src/modules/api-utils/apiResponse.ts` — response shaping used across controllers. - - `src/controllers/CompanyController.ts` — example controller calling managers. - - `src/modules/cw-utils/fetchCompanyConfigurations.ts` — example external integration utility. - - `generated/prisma/client.ts` — generated Prisma client imports; avoid editing. +- `apiResponse.successful(message, data?, meta?)` — 200 +- `apiResponse.created(message, data?)` — 201 +- `apiResponse.error(err)` — reads `status` from the error +- `apiResponse.internalError()` — 500 +- `apiResponse.zodError(err)` — 400 -- **Coding conventions & patterns specific to this repo**: - - Prefer the existing layered architecture: routers → controllers → managers → modules. - - Use the `apiResponse` helpers for all HTTP responses. - - Throw or propagate repository-specific custom errors (from `src/Errors/*`) rather than returning bare objects. - - Keep TypeScript types in `src/types/*` and use Zod for runtime checks. - - **Avoid `else` statements** — prefer ternary operators, early returns, or other control flow patterns. Only use `else` if there is absolutely no other way. +## Auth & external integrations -- **Local dev / quick checks**: - - Start dev server: `npm run dev` - - Regenerate Prisma client: `npm run db:gen` - - Apply DB migrations locally: `npm run db:push` +Microsoft OAuth flow is under `src/api/auth/*` and `src/modules/fetchMicrosoftUser.ts`. If touching authentication, follow existing redirect/refresh patterns in `src/api/auth` and preserve token-refresh semantics. -- **When editing generated or infra files**: if you need to change `generated/prisma/*` (rare), explain why in the PR and show commands used to regenerate. +## ConnectWise integration -- **Documentation sync (IMPORTANT)**: Whenever you add, remove, or modify API routes or permission nodes, you **must** update all three of the following files to keep them in sync: - 1. `src/types/PermissionNodes.ts` — the single source of truth for all permission node definitions, categories, descriptions, and `usedIn` references. - 2. `PERMISSIONS.md` — human-readable documentation of all permission nodes; must strictly reflect the data in `PermissionNodes.ts`. - 3. `API_ROUTES.md` — comprehensive documentation of all API routes, including method, path, auth requirements, permissions, request/response examples. - Always verify that new routes have their required permissions listed in `PermissionNodes.ts`, that `PERMISSIONS.md` tables match the TS file exactly, and that `API_ROUTES.md` includes full documentation for every mounted route. Run through all three files at the end of any route or permission change to catch discrepancies. +Utilities for ConnectWise interactions live in `src/modules/cw-utils/*` (e.g., `configurations/fetchCompanyConfigurations.ts`, `fetchCompany.ts`, `fetchAllCompanies.ts`). These modules call external APIs via the `connectWiseApi` Axios instance from `src/constants.ts` and return domain data; preserve the module contracts (input types and returned shapes) when refactoring. -- **Field-level permission gating (processObjectValuePerms)**: Some routes use `processObjectValuePerms` from `src/modules/permission-utils/processObjectPermissions.ts` to filter response objects on a per-field basis. When this pattern is used, every key of the response object becomes a permission node in the form `.` (e.g., `unifi.site.wifi.read.passphrase`). Only fields whose corresponding permission the user holds are included in the response. +## UniFi integration - **When documenting a route that uses field-level gating, you must:** - 1. Note in `API_ROUTES.md` that the route uses field-level gating, explain the behaviour, and list every `.` permission node in a collapsible table. - 2. Add a `unifi.site.wifi.read`-style parent permission node in `PermissionNodes.ts` with a `fieldLevelPermissions` array listing every `.` node. - 3. Add matching rows/notes to `PERMISSIONS.md` including the full list of field-level nodes. +The `UnifiClient` class in `src/modules/unifi-api/UnifiClient.ts` wraps all UniFi controller API interactions (login, sites, WLANs, devices, networks, AP groups, WLAN groups, speed profiles, PPSKs). The shared instance is exported from `src/constants.ts` as `unifi`. UniFi route handlers live in `src/api/unifi/` with `sites/` (multi-site operations) and `site/` (single-site operations) sub-folders. - **Current routes using field-level gating:** - - `GET /v1/unifi/site/:id/wifi` — scope `unifi.site.wifi.read`, gates every field on the `WlanConf` object. +## Generated files and CI -If anything here is unclear or you'd like more examples (e.g., a walk-through editing a controller + manager + test run), tell me which area to expand and I'll iterate. +`generated/` is a build artifact. Do not modify. When updating Prisma models, run `npm run db:gen` and commit changes to `generated/prisma` only if that's the established workflow. + +--- + +## Coding conventions & patterns specific to this repo + +- Prefer the existing layered architecture: route handlers → managers → controllers (domain models) / modules. +- Use the `createRoute()` utility for all route handler files. +- Use the `apiResponse` helpers for all HTTP responses. +- Throw or propagate repository-specific custom errors (from `src/Errors/*`) rather than returning bare objects. +- Keep TypeScript types in `src/types/*` and use Zod for runtime checks inside route handlers. +- **Avoid `else` statements** — prefer ternary operators, early returns, or other control flow patterns. Only use `else` if there is absolutely no other way. +- ES module syntax (`export default`, `import`) is used throughout. The `require()` calls in `server.ts` are for lazy loading but all modules use `export default`. + +--- + +## Local dev / quick checks + +- Start dev server: `npm run dev` +- Regenerate Prisma client: `npm run db:gen` +- Apply DB migrations locally: `npm run db:push` +- Docker dev utilities: `npm run utils:dev` +- Generate private keys: `npm run utils:gen_private_keys` +- Create admin role: `npm run utils:create_admin_role` +- Assign user role: `npm run utils:assign_user_role` + +## When editing generated or infra files + +If you need to change `generated/prisma/*` (rare), explain why in the PR and show commands used to regenerate. + +--- + +## Documentation sync (IMPORTANT) + +Whenever you add, remove, or modify API routes or permission nodes, you **must** update all three of the following files to keep them in sync: + +1. `src/types/PermissionNodes.ts` — the single source of truth for all permission node definitions, categories, descriptions, and `usedIn` references. +2. `PERMISSIONS.md` — human-readable documentation of all permission nodes; must strictly reflect the data in `PermissionNodes.ts`. +3. `API_ROUTES.md` — comprehensive documentation of all API routes, including method, path, auth requirements, permissions, request/response examples. + +Always verify that new routes have their required permissions listed in `PermissionNodes.ts`, that `PERMISSIONS.md` tables match the TS file exactly, and that `API_ROUTES.md` includes full documentation for every mounted route. Run through all three files at the end of any route or permission change to catch discrepancies. + +--- + +## Field-level permission gating (`processObjectValuePerms`) + +Some routes use `processObjectValuePerms` from `src/modules/permission-utils/processObjectPermissions.ts` to filter response objects on a per-field basis. When this pattern is used, every key of the response object becomes a permission node in the form `.` (e.g., `unifi.site.wifi.read.passphrase`). Only fields whose corresponding permission the user holds are included in the response. + +There is also `processObjectPermMap` in the same file, which returns a `Record` indicating which fields the user has permission for (useful for UI gating). + +**When documenting a route that uses field-level gating, you must:** + +1. Note in `API_ROUTES.md` that the route uses field-level gating, explain the behaviour, and list every `.` permission node in a collapsible table. +2. Add a `unifi.site.wifi.read`-style parent permission node in `PermissionNodes.ts` with a `fieldLevelPermissions` array listing every `.` node. +3. Add matching rows/notes to `PERMISSIONS.md` including the full list of field-level nodes. + +**Current routes using field-level gating:** + +- `GET /v1/unifi/site/:id/wifi` — scope `unifi.site.wifi.read`, gates every field on the `WlanConf` object. + +--- + +If anything here is unclear or you'd like more examples (e.g., a walk-through adding a route handler + manager + controller), tell me which area to expand and I'll iterate. diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml new file mode 100644 index 0000000..8c8ea4b --- /dev/null +++ b/.github/workflows/build-and-publish.yaml @@ -0,0 +1,63 @@ +name: Build and Publish + +on: + release: + types: [created] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push the Docker image + uses: docker/build-push-action@v6 + with: + push: true + tags: | + ghcr.io/project-optima/ttscm-api:latest + ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }} + deploy: + name: Deploy + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Set the Kubernetes context + uses: azure/k8s-set-context@v2 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBECONFIG }} + + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Lint Kubernetes manifests + uses: azure/k8s-lint@v1 + with: + lintType: dryrun + manifests: | + kubernetes/deployment.yaml + namespace: optima + + - name: Deploy to the Kubernetes cluster + uses: azure/k8s-deploy@v5 + with: + namespace: optima + force: true + skip-tls-verify: true + manifests: | + kubernetes/deployment.yaml + images: | + ghcr.io/project-optima/ttscm-api:${{ github.event.release.tag_name }} diff --git a/API_ROUTES.md b/API_ROUTES.md index 8b70882..df5a3ab 100644 --- a/API_ROUTES.md +++ b/API_ROUTES.md @@ -1,6 +1,6 @@ -# TTSCM API Routes Documentation +# Optima API Routes Documentation -This document provides a comprehensive overview of all API routes available in the TTSCM API. +This document provides a comprehensive overview of all API routes available in the Optima API. ## Base URL diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4bcd022 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# ---- Stage 1: Install dependencies ---- +FROM oven/bun:1 AS deps + +WORKDIR /app + +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile --production + +# ---- Stage 2: Build ---- +FROM oven/bun:1 AS build + +WORKDIR /app + +# Copy dependency manifests and install all deps (including dev) +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +# Copy source code and supporting files +COPY src/ src/ +COPY generated/ generated/ +COPY prisma/ prisma/ +COPY public-keys/ public-keys/ +COPY prisma.config.ts tsconfig.json ./ + +# Compile to a standalone executable +RUN bun build src/index.ts \ + --compile \ + --minify \ + --target=bun-linux-x64 \ + --outfile=server + +# ---- Stage 3: Production image ---- +FROM ubuntu:22.04 AS runtime + +WORKDIR /app + +# Install minimal runtime dependencies (CA certs for HTTPS calls) +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Copy the compiled binary from the build stage +COPY --from=build /app/server ./server + +# Copy Prisma artifacts needed at runtime +COPY --from=build /app/generated/ ./generated/ +COPY --from=build /app/prisma/ ./prisma/ +COPY --from=build /app/prisma.config.ts ./prisma.config.ts +COPY --from=build /app/public-keys/ ./public-keys/ + +# Copy production node_modules (Prisma adapter needs native bindings) +COPY --from=deps /app/node_modules/ ./node_modules/ + +ENV NODE_ENV=production +EXPOSE 3000 + +CMD ["./server"] \ No newline at end of file diff --git a/PERMISSIONS.md b/PERMISSIONS.md index 3a07668..e2f7df0 100644 --- a/PERMISSIONS.md +++ b/PERMISSIONS.md @@ -1,6 +1,6 @@ # Permission Nodes -This document lists all known permission nodes in the ttscm-api application, categorized by resource type. +This document lists all known permission nodes in the optima-api application, categorized by resource type. ## Permission System Overview diff --git a/bruno/ttscm/Check User Permission.bru b/bruno/optima/Check User Permission.bru similarity index 100% rename from bruno/ttscm/Check User Permission.bru rename to bruno/optima/Check User Permission.bru diff --git a/bruno/ttscm/Fetch Company Pages.bru b/bruno/optima/Fetch Company Pages.bru similarity index 100% rename from bruno/ttscm/Fetch Company Pages.bru rename to bruno/optima/Fetch Company Pages.bru diff --git a/bruno/ttscm/Teapot.bru b/bruno/optima/Teapot.bru similarity index 100% rename from bruno/ttscm/Teapot.bru rename to bruno/optima/Teapot.bru diff --git a/bruno/optima/bruno.json b/bruno/optima/bruno.json new file mode 100644 index 0000000..96a8a99 --- /dev/null +++ b/bruno/optima/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "optima", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/bruno/ttscm/bruno.json b/bruno/ttscm/bruno.json index b6108ee..3b4e019 100644 --- a/bruno/ttscm/bruno.json +++ b/bruno/ttscm/bruno.json @@ -1,9 +1,6 @@ { "version": "1", - "name": "ttscm", + "name": "optima", "type": "collection", - "ignore": [ - "node_modules", - ".git" - ] -} \ No newline at end of file + "ignore": ["node_modules", ".git"] +} diff --git a/kubernetes/deployment.yaml b/kubernetes/deployment.yaml new file mode 100644 index 0000000..9ee6f88 --- /dev/null +++ b/kubernetes/deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: optima-api + namespace: optima +spec: + selector: + matchLabels: + app: optima-api + replicas: 1 + template: + metadata: + labels: + app: optima-api + spec: + containers: + - name: optima-api + image: ghcr.io/project-optima/ttscm-api:latest + imagePullPolicy: Always + envFrom: + - secretRef: + name: optima-api-env + ports: + - containerPort: 3000 + imagePullSecrets: + - name: github-container-registry diff --git a/kubernetes/ingress.yaml b/kubernetes/ingress.yaml new file mode 100644 index 0000000..f31cfb8 --- /dev/null +++ b/kubernetes/ingress.yaml @@ -0,0 +1,36 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: optima-api-ingress + namespace: optima + annotations: + ingress.kubernetes.io/ssl-redirect: "false" +spec: + tls: + - secretName: osdci-net-cert + rules: + - host: opt-api.osdci.net + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: optima-api + port: + number: 3000 +--- +apiVersion: v1 +kind: Service +metadata: + name: optima-api + namespace: optima + labels: + app: optima-api +spec: + type: ClusterIP + ports: + - port: 3000 + protocol: TCP + selector: + app: optima-api diff --git a/package.json b/package.json index 49c234d..3d1fc93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "tts-optima-api", "homepage": "https://totaltech.net", + "version": "v0.1.0", "author": { "name": "Jackson Roberts", "email": "jackson.roberts@totaltech.net", diff --git a/utils/genProductionKeys.ts b/utils/genProductionKeys.ts index fcdf786..2112591 100644 --- a/utils/genProductionKeys.ts +++ b/utils/genProductionKeys.ts @@ -38,7 +38,7 @@ const toBase64 = (str: string) => Buffer.from(str).toString("base64"); const secretYaml = `apiVersion: v1 kind: Secret metadata: - name: ttscm-keys + name: optima-keys type: Opaque data: accessToken.key: ${toBase64(generatedKeys["accessToken"].private)} @@ -48,7 +48,7 @@ data: secureValues.pub: ${toBase64(generatedKeys["secureValues"].public)} `; -const secretPath = `${outputDir}/ttscm-keys-secret.yaml`; +const secretPath = `${outputDir}/optima-keys-secret.yaml`; await Bun.write(secretPath, secretYaml); console.log(`\n ✔ ${secretPath}`);