add docs
This commit is contained in:
parent
127b19b4e1
commit
708d23df22
@ -18,6 +18,7 @@
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"dependencies": {
|
||||
"@fastify/swagger": "^9.7.0",
|
||||
"dotenv": "^17.2.4",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"fastify": "^5.7.4",
|
||||
|
||||
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@fastify/swagger':
|
||||
specifier: ^9.7.0
|
||||
version: 9.7.0
|
||||
dotenv:
|
||||
specifier: ^17.2.4
|
||||
version: 17.2.4
|
||||
@ -38,7 +41,7 @@ importers:
|
||||
version: 25.2.1
|
||||
'@vitest/coverage-v8':
|
||||
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:
|
||||
specifier: ^0.31.8
|
||||
version: 0.31.8
|
||||
@ -50,7 +53,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
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:
|
||||
|
||||
@ -548,6 +551,9 @@ packages:
|
||||
'@fastify/proxy-addr@5.1.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@ -1007,6 +1013,10 @@ packages:
|
||||
json-schema-ref-resolver@3.0.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
@ -1069,6 +1079,9 @@ packages:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
openapi-types@12.1.3:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
@ -1300,6 +1313,11 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
yaml@2.8.2:
|
||||
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
|
||||
@ -1577,6 +1595,16 @@ snapshots:
|
||||
'@fastify/forwarded': 3.0.1
|
||||
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/sourcemap-codec@1.5.5': {}
|
||||
@ -1685,7 +1713,7 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.0.18
|
||||
@ -1697,7 +1725,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 3.10.0
|
||||
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':
|
||||
dependencies:
|
||||
@ -1708,13 +1736,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
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:
|
||||
'@vitest/spy': 4.0.18
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
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':
|
||||
dependencies:
|
||||
@ -1982,6 +2010,14 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
jsonwebtoken@9.0.3:
|
||||
@ -2050,6 +2086,8 @@ snapshots:
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
openapi-types@12.1.3: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@ -2200,7 +2238,7 @@ snapshots:
|
||||
|
||||
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:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@ -2212,11 +2250,12 @@ snapshots:
|
||||
'@types/node': 25.2.1
|
||||
fsevents: 2.3.3
|
||||
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:
|
||||
'@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/runner': 4.0.18
|
||||
'@vitest/snapshot': 4.0.18
|
||||
@ -2233,7 +2272,7 @@ snapshots:
|
||||
tinyexec: 1.0.2
|
||||
tinyglobby: 0.2.15
|
||||
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
|
||||
optionalDependencies:
|
||||
'@types/node': 25.2.1
|
||||
@ -2255,4 +2294,6 @@ snapshots:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
zod@4.3.6: {}
|
||||
|
||||
30
src/app.ts
30
src/app.ts
@ -1,8 +1,10 @@
|
||||
import Fastify from "fastify";
|
||||
import fastifySwagger from "@fastify/swagger";
|
||||
import { client } from "./db/index.js";
|
||||
import { errorHandler } from "./plugins/error-handler.js";
|
||||
import { authenticate } from "./plugins/authenticate.js";
|
||||
import { accountContext } from "./plugins/account-context.js";
|
||||
import scalarDocs from "./plugins/scalar-docs.js";
|
||||
import { registerRoutes } from "./routes/index.js";
|
||||
|
||||
export function buildApp() {
|
||||
@ -14,6 +16,34 @@ export function buildApp() {
|
||||
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(authenticate);
|
||||
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) {
|
||||
// GET /accounts — list user's accounts with role
|
||||
app.get("/accounts", { preHandler: [app.authenticate] }, async (request) => {
|
||||
return getUserAccounts(request.user.sub);
|
||||
});
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
||||
// POST /accounts — create a new account (user becomes owner)
|
||||
app.post<{ Body: { name: string } }>(
|
||||
@ -15,6 +47,9 @@ export async function accountRoutes(app: FastifyInstance) {
|
||||
{
|
||||
preHandler: [app.authenticate],
|
||||
schema: {
|
||||
description: "Create a new account",
|
||||
tags: ["Accounts"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
@ -22,6 +57,24 @@ export async function accountRoutes(app: FastifyInstance) {
|
||||
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) => {
|
||||
|
||||
@ -1,7 +1,25 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
|
||||
export async function healthRoutes(app: FastifyInstance) {
|
||||
app.get("/health", async () => {
|
||||
return { status: "ok" };
|
||||
});
|
||||
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" };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ export async function loginRoutes(app: FastifyInstance) {
|
||||
"/login",
|
||||
{
|
||||
schema: {
|
||||
description: "Request an OTP (One-Time Password) for login",
|
||||
tags: ["Authentication"],
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["email"],
|
||||
@ -14,6 +16,22 @@ export async function loginRoutes(app: FastifyInstance) {
|
||||
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) => {
|
||||
|
||||
@ -6,18 +6,65 @@ import { getUserAccounts } from "../lib/accounts.js";
|
||||
|
||||
export async function meRoutes(app: FastifyInstance) {
|
||||
// GET /me — return current user with accounts (authenticated)
|
||||
app.get("/me", { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub;
|
||||
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 [user] = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user) {
|
||||
return reply.status(404).send({ error: "User not found" });
|
||||
}
|
||||
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user) {
|
||||
return reply.status(404).send({ error: "User not found" });
|
||||
}
|
||||
|
||||
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)
|
||||
app.patch<{ Body: { name: string } }>(
|
||||
@ -25,6 +72,9 @@ export async function meRoutes(app: FastifyInstance) {
|
||||
{
|
||||
preHandler: [app.authenticate],
|
||||
schema: {
|
||||
description: "Update current user's profile",
|
||||
tags: ["Users"],
|
||||
security: [{ bearerAuth: [] }],
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
@ -32,6 +82,31 @@ export async function meRoutes(app: FastifyInstance) {
|
||||
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) => {
|
||||
|
||||
@ -12,6 +12,8 @@ export async function sessionRoutes(app: FastifyInstance) {
|
||||
"/",
|
||||
{
|
||||
schema: {
|
||||
description: "Create a new session for an existing user with OTP verification",
|
||||
tags: ["Authentication"],
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["email", "code"],
|
||||
@ -20,6 +22,49 @@ export async function sessionRoutes(app: FastifyInstance) {
|
||||
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) => {
|
||||
@ -51,6 +96,8 @@ export async function sessionRoutes(app: FastifyInstance) {
|
||||
"/refresh",
|
||||
{
|
||||
schema: {
|
||||
description: "Refresh access token using a refresh token",
|
||||
tags: ["Authentication"],
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["refreshToken"],
|
||||
@ -58,6 +105,42 @@ export async function sessionRoutes(app: FastifyInstance) {
|
||||
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) => {
|
||||
@ -102,14 +185,37 @@ export async function sessionRoutes(app: FastifyInstance) {
|
||||
);
|
||||
|
||||
// POST /sessions/logout — revoke all sessions (authenticated)
|
||||
app.post("/logout", { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
const userId = request.user.sub;
|
||||
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;
|
||||
|
||||
await db
|
||||
.update(sessions)
|
||||
.set({ revokedAt: new Date() })
|
||||
.where(and(eq(sessions.userId, userId), isNull(sessions.revokedAt)));
|
||||
await db
|
||||
.update(sessions)
|
||||
.set({ revokedAt: new Date() })
|
||||
.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",
|
||||
{
|
||||
schema: {
|
||||
description: "Sign up a new user with OTP verification",
|
||||
tags: ["Authentication"],
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["email", "code", "accountName"],
|
||||
@ -21,6 +23,49 @@ export async function signupRoutes(app: FastifyInstance) {
|
||||
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) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user