add docs
This commit is contained in:
parent
127b19b4e1
commit
708d23df22
@ -18,6 +18,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@10.18.3",
|
"packageManager": "pnpm@10.18.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/swagger": "^9.7.0",
|
||||||
"dotenv": "^17.2.4",
|
"dotenv": "^17.2.4",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"fastify": "^5.7.4",
|
"fastify": "^5.7.4",
|
||||||
|
|||||||
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@fastify/swagger':
|
||||||
|
specifier: ^9.7.0
|
||||||
|
version: 9.7.0
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.2.4
|
specifier: ^17.2.4
|
||||||
version: 17.2.4
|
version: 17.2.4
|
||||||
@ -38,7 +41,7 @@ importers:
|
|||||||
version: 25.2.1
|
version: 25.2.1
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^4.0.18
|
specifier: ^4.0.18
|
||||||
version: 4.0.18(vitest@4.0.18(@types/node@25.2.1)(tsx@4.21.0))
|
version: 4.0.18(vitest@4.0.18(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
drizzle-kit:
|
drizzle-kit:
|
||||||
specifier: ^0.31.8
|
specifier: ^0.31.8
|
||||||
version: 0.31.8
|
version: 0.31.8
|
||||||
@ -50,7 +53,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.18
|
specifier: ^4.0.18
|
||||||
version: 4.0.18(@types/node@25.2.1)(tsx@4.21.0)
|
version: 4.0.18(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -548,6 +551,9 @@ packages:
|
|||||||
'@fastify/proxy-addr@5.1.0':
|
'@fastify/proxy-addr@5.1.0':
|
||||||
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
|
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
|
||||||
|
|
||||||
|
'@fastify/swagger@9.7.0':
|
||||||
|
resolution: {integrity: sha512-Vp1SC1GC2Hrkd3faFILv86BzUNyFz5N4/xdExqtCgkGASOzn/x+eMe4qXIGq7cdT6wif/P/oa6r1Ruqx19paZA==}
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2':
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@ -1007,6 +1013,10 @@ packages:
|
|||||||
json-schema-ref-resolver@3.0.0:
|
json-schema-ref-resolver@3.0.0:
|
||||||
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
|
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
|
||||||
|
|
||||||
|
json-schema-resolver@3.0.0:
|
||||||
|
resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
json-schema-traverse@1.0.0:
|
json-schema-traverse@1.0.0:
|
||||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||||
|
|
||||||
@ -1069,6 +1079,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
openapi-types@12.1.3:
|
||||||
|
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||||
|
|
||||||
pathe@2.0.3:
|
pathe@2.0.3:
|
||||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
@ -1300,6 +1313,11 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
yaml@2.8.2:
|
||||||
|
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
|
||||||
|
engines: {node: '>= 14.6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
zod@4.3.6:
|
zod@4.3.6:
|
||||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||||
|
|
||||||
@ -1577,6 +1595,16 @@ snapshots:
|
|||||||
'@fastify/forwarded': 3.0.1
|
'@fastify/forwarded': 3.0.1
|
||||||
ipaddr.js: 2.3.0
|
ipaddr.js: 2.3.0
|
||||||
|
|
||||||
|
'@fastify/swagger@9.7.0':
|
||||||
|
dependencies:
|
||||||
|
fastify-plugin: 5.1.0
|
||||||
|
json-schema-resolver: 3.0.0
|
||||||
|
openapi-types: 12.1.3
|
||||||
|
rfdc: 1.4.1
|
||||||
|
yaml: 2.8.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@jridgewell/resolve-uri@3.1.2': {}
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
@ -1685,7 +1713,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
'@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.2.1)(tsx@4.21.0))':
|
'@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@bcoe/v8-coverage': 1.0.2
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
'@vitest/utils': 4.0.18
|
'@vitest/utils': 4.0.18
|
||||||
@ -1697,7 +1725,7 @@ snapshots:
|
|||||||
obug: 2.1.1
|
obug: 2.1.1
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vitest: 4.0.18(@types/node@25.2.1)(tsx@4.21.0)
|
vitest: 4.0.18(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
'@vitest/expect@4.0.18':
|
'@vitest/expect@4.0.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1708,13 +1736,13 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.1)(tsx@4.21.0))':
|
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.0.18
|
'@vitest/spy': 4.0.18
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.3.1(@types/node@25.2.1)(tsx@4.21.0)
|
vite: 7.3.1(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
'@vitest/pretty-format@4.0.18':
|
'@vitest/pretty-format@4.0.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1982,6 +2010,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
|
|
||||||
|
json-schema-resolver@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
fast-uri: 3.1.0
|
||||||
|
rfdc: 1.4.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
json-schema-traverse@1.0.0: {}
|
||||||
|
|
||||||
jsonwebtoken@9.0.3:
|
jsonwebtoken@9.0.3:
|
||||||
@ -2050,6 +2086,8 @@ snapshots:
|
|||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
on-exit-leak-free@2.1.2: {}
|
||||||
|
|
||||||
|
openapi-types@12.1.3: {}
|
||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@ -2200,7 +2238,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.16.0: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
vite@7.3.1(@types/node@25.2.1)(tsx@4.21.0):
|
vite@7.3.1(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.3
|
esbuild: 0.27.3
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@ -2212,11 +2250,12 @@ snapshots:
|
|||||||
'@types/node': 25.2.1
|
'@types/node': 25.2.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
|
yaml: 2.8.2
|
||||||
|
|
||||||
vitest@4.0.18(@types/node@25.2.1)(tsx@4.21.0):
|
vitest@4.0.18(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.0.18
|
'@vitest/expect': 4.0.18
|
||||||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.1)(tsx@4.21.0))
|
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
'@vitest/pretty-format': 4.0.18
|
'@vitest/pretty-format': 4.0.18
|
||||||
'@vitest/runner': 4.0.18
|
'@vitest/runner': 4.0.18
|
||||||
'@vitest/snapshot': 4.0.18
|
'@vitest/snapshot': 4.0.18
|
||||||
@ -2233,7 +2272,7 @@ snapshots:
|
|||||||
tinyexec: 1.0.2
|
tinyexec: 1.0.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vite: 7.3.1(@types/node@25.2.1)(tsx@4.21.0)
|
vite: 7.3.1(@types/node@25.2.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.2.1
|
'@types/node': 25.2.1
|
||||||
@ -2255,4 +2294,6 @@ snapshots:
|
|||||||
siginfo: 2.0.0
|
siginfo: 2.0.0
|
||||||
stackback: 0.0.2
|
stackback: 0.0.2
|
||||||
|
|
||||||
|
yaml@2.8.2: {}
|
||||||
|
|
||||||
zod@4.3.6: {}
|
zod@4.3.6: {}
|
||||||
|
|||||||
30
src/app.ts
30
src/app.ts
@ -1,8 +1,10 @@
|
|||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
|
import fastifySwagger from "@fastify/swagger";
|
||||||
import { client } from "./db/index.js";
|
import { client } from "./db/index.js";
|
||||||
import { errorHandler } from "./plugins/error-handler.js";
|
import { errorHandler } from "./plugins/error-handler.js";
|
||||||
import { authenticate } from "./plugins/authenticate.js";
|
import { authenticate } from "./plugins/authenticate.js";
|
||||||
import { accountContext } from "./plugins/account-context.js";
|
import { accountContext } from "./plugins/account-context.js";
|
||||||
|
import scalarDocs from "./plugins/scalar-docs.js";
|
||||||
import { registerRoutes } from "./routes/index.js";
|
import { registerRoutes } from "./routes/index.js";
|
||||||
|
|
||||||
export function buildApp() {
|
export function buildApp() {
|
||||||
@ -14,6 +16,34 @@ export function buildApp() {
|
|||||||
await client.end();
|
await client.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.register(fastifySwagger, {
|
||||||
|
openapi: {
|
||||||
|
openapi: "3.0.0",
|
||||||
|
info: {
|
||||||
|
title: "Eyrun API",
|
||||||
|
description: "Authentication and account management API",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
description: "Development server",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
bearerAuth: {
|
||||||
|
type: "http",
|
||||||
|
scheme: "bearer",
|
||||||
|
bearerFormat: "JWT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.register(scalarDocs);
|
||||||
|
|
||||||
app.register(errorHandler);
|
app.register(errorHandler);
|
||||||
app.register(authenticate);
|
app.register(authenticate);
|
||||||
app.register(accountContext);
|
app.register(accountContext);
|
||||||
|
|||||||
46
src/plugins/scalar-docs.ts
Normal file
46
src/plugins/scalar-docs.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import fastifyPlugin from "fastify-plugin";
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
|
||||||
|
export default fastifyPlugin(
|
||||||
|
async function (app: FastifyInstance) {
|
||||||
|
// Expose OpenAPI spec at /docs/json
|
||||||
|
app.get("/docs/json", async () => {
|
||||||
|
return app.swagger();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scalar UI at root
|
||||||
|
app.get("/", async (request, reply) => {
|
||||||
|
const title = "Eyrun API";
|
||||||
|
return reply.type("text/html").send(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>${title}</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script
|
||||||
|
id="api-reference"
|
||||||
|
data-url="/docs/json"
|
||||||
|
src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep /docs as an alias for convenience
|
||||||
|
app.get("/docs", async (request, reply) => {
|
||||||
|
return reply.redirect("/");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scalar-docs",
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -5,9 +5,41 @@ import { getUserAccounts } from "../lib/accounts.js";
|
|||||||
|
|
||||||
export async function accountRoutes(app: FastifyInstance) {
|
export async function accountRoutes(app: FastifyInstance) {
|
||||||
// GET /accounts — list user's accounts with role
|
// GET /accounts — list user's accounts with role
|
||||||
app.get("/accounts", { preHandler: [app.authenticate] }, async (request) => {
|
app.get(
|
||||||
|
"/accounts",
|
||||||
|
{
|
||||||
|
preHandler: [app.authenticate],
|
||||||
|
schema: {
|
||||||
|
description: "List all accounts for the current user",
|
||||||
|
tags: ["Accounts"],
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
description: "List of user accounts",
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
role: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "Unauthorized",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request) => {
|
||||||
return getUserAccounts(request.user.sub);
|
return getUserAccounts(request.user.sub);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// POST /accounts — create a new account (user becomes owner)
|
// POST /accounts — create a new account (user becomes owner)
|
||||||
app.post<{ Body: { name: string } }>(
|
app.post<{ Body: { name: string } }>(
|
||||||
@ -15,6 +47,9 @@ export async function accountRoutes(app: FastifyInstance) {
|
|||||||
{
|
{
|
||||||
preHandler: [app.authenticate],
|
preHandler: [app.authenticate],
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create a new account",
|
||||||
|
tags: ["Accounts"],
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["name"],
|
required: ["name"],
|
||||||
@ -22,6 +57,24 @@ export async function accountRoutes(app: FastifyInstance) {
|
|||||||
name: { type: "string", minLength: 1 },
|
name: { type: "string", minLength: 1 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
response: {
|
||||||
|
201: {
|
||||||
|
description: "Account created successfully",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
role: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "Unauthorized",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
|
|||||||
@ -1,7 +1,25 @@
|
|||||||
import type { FastifyInstance } from "fastify";
|
import type { FastifyInstance } from "fastify";
|
||||||
|
|
||||||
export async function healthRoutes(app: FastifyInstance) {
|
export async function healthRoutes(app: FastifyInstance) {
|
||||||
app.get("/health", async () => {
|
app.get(
|
||||||
|
"/health",
|
||||||
|
{
|
||||||
|
schema: {
|
||||||
|
description: "Health check endpoint",
|
||||||
|
tags: ["Health"],
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
description: "Service is healthy",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
return { status: "ok" };
|
return { status: "ok" };
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@ export async function loginRoutes(app: FastifyInstance) {
|
|||||||
"/login",
|
"/login",
|
||||||
{
|
{
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Request an OTP (One-Time Password) for login",
|
||||||
|
tags: ["Authentication"],
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["email"],
|
required: ["email"],
|
||||||
@ -14,6 +16,22 @@ export async function loginRoutes(app: FastifyInstance) {
|
|||||||
email: { type: "string", format: "email" },
|
email: { type: "string", format: "email" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
description: "OTP sent successfully",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
message: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
429: {
|
||||||
|
description: "Too many OTP requests",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
|
|||||||
@ -6,7 +6,53 @@ import { getUserAccounts } from "../lib/accounts.js";
|
|||||||
|
|
||||||
export async function meRoutes(app: FastifyInstance) {
|
export async function meRoutes(app: FastifyInstance) {
|
||||||
// GET /me — return current user with accounts (authenticated)
|
// GET /me — return current user with accounts (authenticated)
|
||||||
app.get("/me", { preHandler: [app.authenticate] }, async (request, reply) => {
|
app.get(
|
||||||
|
"/me",
|
||||||
|
{
|
||||||
|
preHandler: [app.authenticate],
|
||||||
|
schema: {
|
||||||
|
description: "Get current authenticated user profile and accounts",
|
||||||
|
tags: ["Users"],
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
description: "Current user with accounts",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
email: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
accounts: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
role: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "Unauthorized",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: "User not found",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
const userId = request.user.sub;
|
const userId = request.user.sub;
|
||||||
|
|
||||||
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
||||||
@ -17,7 +63,8 @@ export async function meRoutes(app: FastifyInstance) {
|
|||||||
const userAccounts = await getUserAccounts(userId);
|
const userAccounts = await getUserAccounts(userId);
|
||||||
|
|
||||||
return { ...user, accounts: userAccounts };
|
return { ...user, accounts: userAccounts };
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// PATCH /me — update current user's profile (authenticated)
|
// PATCH /me — update current user's profile (authenticated)
|
||||||
app.patch<{ Body: { name: string } }>(
|
app.patch<{ Body: { name: string } }>(
|
||||||
@ -25,6 +72,9 @@ export async function meRoutes(app: FastifyInstance) {
|
|||||||
{
|
{
|
||||||
preHandler: [app.authenticate],
|
preHandler: [app.authenticate],
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update current user's profile",
|
||||||
|
tags: ["Users"],
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["name"],
|
required: ["name"],
|
||||||
@ -32,6 +82,31 @@ export async function meRoutes(app: FastifyInstance) {
|
|||||||
name: { type: "string", minLength: 1 },
|
name: { type: "string", minLength: 1 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
description: "User updated successfully",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
email: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "Unauthorized",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: "User not found",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
|
|||||||
@ -12,6 +12,8 @@ export async function sessionRoutes(app: FastifyInstance) {
|
|||||||
"/",
|
"/",
|
||||||
{
|
{
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create a new session for an existing user with OTP verification",
|
||||||
|
tags: ["Authentication"],
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["email", "code"],
|
required: ["email", "code"],
|
||||||
@ -20,6 +22,49 @@ export async function sessionRoutes(app: FastifyInstance) {
|
|||||||
code: { type: "string", minLength: 6, maxLength: 6 },
|
code: { type: "string", minLength: 6, maxLength: 6 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
description: "Session created successfully",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
accessToken: { type: "string" },
|
||||||
|
refreshToken: { type: "string" },
|
||||||
|
user: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
email: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accounts: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
role: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: "Invalid or expired OTP",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "User not found or invalid credentials",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
@ -51,6 +96,8 @@ export async function sessionRoutes(app: FastifyInstance) {
|
|||||||
"/refresh",
|
"/refresh",
|
||||||
{
|
{
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Refresh access token using a refresh token",
|
||||||
|
tags: ["Authentication"],
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["refreshToken"],
|
required: ["refreshToken"],
|
||||||
@ -58,6 +105,42 @@ export async function sessionRoutes(app: FastifyInstance) {
|
|||||||
refreshToken: { type: "string", minLength: 1 },
|
refreshToken: { type: "string", minLength: 1 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
description: "New tokens generated successfully",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
accessToken: { type: "string" },
|
||||||
|
refreshToken: { type: "string" },
|
||||||
|
user: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
email: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accounts: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
role: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "Invalid, expired, or reused refresh token",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
@ -102,7 +185,29 @@ export async function sessionRoutes(app: FastifyInstance) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// POST /sessions/logout — revoke all sessions (authenticated)
|
// POST /sessions/logout — revoke all sessions (authenticated)
|
||||||
app.post("/logout", { preHandler: [app.authenticate] }, async (request, reply) => {
|
app.post(
|
||||||
|
"/logout",
|
||||||
|
{
|
||||||
|
preHandler: [app.authenticate],
|
||||||
|
schema: {
|
||||||
|
description: "Revoke all sessions for the current user (logout)",
|
||||||
|
tags: ["Authentication"],
|
||||||
|
security: [{ bearerAuth: [] }],
|
||||||
|
response: {
|
||||||
|
204: {
|
||||||
|
description: "Successfully logged out",
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "Unauthorized",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
const userId = request.user.sub;
|
const userId = request.user.sub;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
@ -111,5 +216,6 @@ export async function sessionRoutes(app: FastifyInstance) {
|
|||||||
.where(and(eq(sessions.userId, userId), isNull(sessions.revokedAt)));
|
.where(and(eq(sessions.userId, userId), isNull(sessions.revokedAt)));
|
||||||
|
|
||||||
return reply.status(204).send();
|
return reply.status(204).send();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,8 @@ export async function signupRoutes(app: FastifyInstance) {
|
|||||||
"/signup",
|
"/signup",
|
||||||
{
|
{
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Sign up a new user with OTP verification",
|
||||||
|
tags: ["Authentication"],
|
||||||
body: {
|
body: {
|
||||||
type: "object",
|
type: "object",
|
||||||
required: ["email", "code", "accountName"],
|
required: ["email", "code", "accountName"],
|
||||||
@ -21,6 +23,49 @@ export async function signupRoutes(app: FastifyInstance) {
|
|||||||
accountName: { type: "string", minLength: 1 },
|
accountName: { type: "string", minLength: 1 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
response: {
|
||||||
|
201: {
|
||||||
|
description: "User created successfully",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
accessToken: { type: "string" },
|
||||||
|
refreshToken: { type: "string" },
|
||||||
|
user: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
email: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accounts: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string" },
|
||||||
|
name: { type: "string" },
|
||||||
|
role: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: "Invalid or expired OTP",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
409: {
|
||||||
|
description: "User already exists",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user