Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 40 additions & 53 deletions apps/backend/src/handlers/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Elysia } from "elysia";
import { ip } from "elysia-ip";
import { rateLimit } from "elysia-rate-limit";
import { RateLimitError } from "@common/error";
import {
AppError,
auth,
Expand All @@ -16,60 +14,49 @@ import { traceTask } from "@/common/trace";

const DAY = 86400;

export const loginHandler = new Elysia()
.use(ip())
.use(
rateLimit({
scoping: "scoped",
max: 50,
duration: 5 * 60 * 1000,
generator: () => "",
errorResponse: new RateLimitError(),
})
)
.post(
"/session",
async ({ body, status, headers, cookie: { token: tokenCookie } }) => {
const { user, token } = await traceTask("auth.signInEmail", async () => {
return await auth.api.signInEmail({
body: {
email: body.email,
password: body.password,
},
headers: toBetterAuthHeaders(headers),
});
export const loginHandler = new Elysia().use(ip()).post(
"/session",
async ({ body, status, headers, cookie: { token: tokenCookie } }) => {
const { user, token } = await traceTask("auth.signInEmail", async () => {
return await auth.api.signInEmail({
body: {
email: body.email,
password: body.password,
},
headers: toBetterAuthHeaders(headers),
});
});

if (!token) {
throw new AppError("error.login.failed", "INTERNAL_SERVER_ERROR", 500, {
cause: "Better Auth responded with no token",
});
}
if (!token) {
throw new AppError("error.login.failed", "INTERNAL_SERVER_ERROR", 500, {
cause: "Better Auth responded with no token",
});
}

tokenCookie.value = token;
tokenCookie.httpOnly = true;
tokenCookie.maxAge = 90 * DAY;
tokenCookie.secure = true;
tokenCookie.sameSite = "lax";
tokenCookie.value = token;
tokenCookie.httpOnly = true;
tokenCookie.maxAge = 90 * DAY;
tokenCookie.secure = true;
tokenCookie.sameSite = "lax";

const userInfo = betterAuthToLoginUserInfoDto(user, token);
const response = toLoginResponse(userInfo);
const userInfo = betterAuthToLoginUserInfoDto(user, token);
const response = toLoginResponse(userInfo);

return status(200, response);
return status(200, response);
},
{
body: LoginRequestSchema,
detail: {
summary: "User Login",
description:
"Authenticate an existing user with email and password credentials. Returns user info and sets an httpOnly authentication cookie.",
},
{
body: LoginRequestSchema,
detail: {
summary: "User Login",
description:
"Authenticate an existing user with email and password credentials. Returns user info and sets an httpOnly authentication cookie.",
},
response: {
200: LoginResponseSchema,
401: ErrorResponseSchema,
422: ErrorResponseSchema,
429: ErrorResponseSchema,
500: ErrorResponseSchema,
},
}
);
response: {
200: LoginResponseSchema,
401: ErrorResponseSchema,
422: ErrorResponseSchema,
429: ErrorResponseSchema,
500: ErrorResponseSchema,
},
}
);
Comment on lines +17 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reintroduce throttling on the login endpoint.

POST /session is now unthrottled, which materially increases brute-force and credential-stuffing risk on a critical auth surface. Please restore per-IP/per-identifier rate limiting here (or wire an equivalent guaranteed control at the edge and document it in code).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/auth/login.ts` around lines 17 - 62, The login
endpoint (loginHandler for POST "/session") currently lacks throttling and needs
per-IP and per-identifier rate limiting reinstated: add a rate-limit
check/middleware into the request pipeline for loginHandler (e.g., immediately
before the handler where ip() is used) that enforces a strict per-IP limit and a
per-identifier (email/username) limit, return appropriate 429 responses on
exceed, and document the control in the route detail; ensure the check uses the
incoming headers/body to key identifier-based limits and integrates with your
existing rate-limit provider or an in-memory/fallback store so the protection is
enforced at the application level if not guaranteed at the edge.

87 changes: 37 additions & 50 deletions apps/backend/src/handlers/auth/signup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Elysia } from "elysia";
import { ip } from "elysia-ip";
import { rateLimit } from "elysia-rate-limit";
import {
betterAuthToSignupUserInfoDto,
SignupRequestSchema,
Expand All @@ -10,64 +9,52 @@ import {
ErrorResponseSchema,
toBetterAuthHeaders,
} from "@cvsa/core";
import { RateLimitError } from "@common/error";
import { AppError } from "@cvsa/core";
import { auth } from "@cvsa/core";
import { traceTask } from "@/common/trace";

const DAY = 86400;

export const signupHandler = new Elysia()
.use(ip())
.use(
rateLimit({
scoping: "scoped",
max: 50,
duration: 5 * 60 * 1000,
generator: () => "", // global limit
errorResponse: new RateLimitError(),
})
)
.post(
"/user",
async ({ body, status, headers, cookie: { token: tokenCookie } }) => {
const { user, token } = await traceTask("auth.signUpEmail", async () => {
return await auth.api.signUpEmail({
body: signupRequestToBetterAuth(body),
headers: toBetterAuthHeaders(headers),
});
export const signupHandler = new Elysia().use(ip()).post(
"/user",
async ({ body, status, headers, cookie: { token: tokenCookie } }) => {
const { user, token } = await traceTask("auth.signUpEmail", async () => {
return await auth.api.signUpEmail({
body: signupRequestToBetterAuth(body),
headers: toBetterAuthHeaders(headers),
});
});

if (!token) {
throw new AppError("error.signup.failed", "INTERNAL_SERVER_ERROR", 500, {
cause: "Better Auth responded with no token",
});
}
if (!token) {
throw new AppError("error.signup.failed", "INTERNAL_SERVER_ERROR", 500, {
cause: "Better Auth responded with no token",
});
}

tokenCookie.value = token;
tokenCookie.httpOnly = true;
tokenCookie.maxAge = 90 * DAY;
tokenCookie.secure = true;
tokenCookie.sameSite = "lax";
tokenCookie.value = token;
tokenCookie.httpOnly = true;
tokenCookie.maxAge = 90 * DAY;
tokenCookie.secure = true;
tokenCookie.sameSite = "lax";

const userInfo = betterAuthToSignupUserInfoDto(user, token);
const response = toSignUpResponse(userInfo);
const userInfo = betterAuthToSignupUserInfoDto(user, token);
const response = toSignUpResponse(userInfo);

return status(200, response);
return status(200, response);
},
{
body: SignupRequestSchema,
detail: {
summary: "User Registration",
description:
"Register a new user account with email and password. Returns the created user info and sets an httpOnly authentication cookie.",
},
{
body: SignupRequestSchema,
detail: {
summary: "User Registration",
description:
"Register a new user account with email and password. Returns the created user info and sets an httpOnly authentication cookie.",
},
response: {
200: SignupResponseSchema,
400: ErrorResponseSchema,
422: ErrorResponseSchema,
429: ErrorResponseSchema,
500: ErrorResponseSchema,
},
}
);
response: {
200: SignupResponseSchema,
400: ErrorResponseSchema,
422: ErrorResponseSchema,
429: ErrorResponseSchema,
500: ErrorResponseSchema,
},
}
);
Comment on lines +18 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep anti-abuse throttling on signup.

POST /user no longer has explicit rate limiting, which weakens abuse protection for automated account creation. Please add route-level throttling back (or enforce a guaranteed equivalent upstream control).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/auth/signup.ts` around lines 18 - 60, The signup
route lost rate-limiting—reintroduce route-level throttling on the POST "/user"
handler (signupHandler) by applying a rate-limit/throttle middleware (or
middleware factory) to the Elysia instance or directly to the post("/user")
route; configure it to key by IP (use the existing ip() middleware or headers),
set a conservative limit (e.g., small requests per minute and a short
ban/penalty window), and ensure the middleware returns appropriate 429 responses
and is included in the route options so automated signups are rate-limited while
legitimate users are unaffected.

33 changes: 33 additions & 0 deletions apps/backend/src/handlers/catalog/artistRole/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Elysia } from "elysia";
import {
CreateArtistRoleRequestSchema,
ErrorResponseSchema,
ArtistRoleResponseSchema,
artistRoleService,
} from "@cvsa/core";
import { authMiddleware } from "@/middlewares";
import { traceTask } from "@/common/trace";

export const artistRoleCreateHandler = new Elysia({ name: "artistRoleCreateHandler" })
.use(authMiddleware)
.post(
"/artist-role",
async ({ body, status }) => {
const role = await traceTask("artistRoleService.create", async () => {
return await artistRoleService.create(body);
});
return status(201, role);
},
{
body: CreateArtistRoleRequestSchema,
detail: {
summary: "Create Artist Role",
description:
"Create a new artist role entry in the catalog. Requires authentication.",
},
response: {
201: ArtistRoleResponseSchema,
401: ErrorResponseSchema,
},
Comment on lines +28 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document 400 validation errors in the create route response map.

This route validates body, so bad payloads can return 400 at runtime. Adding 400 here keeps the OpenAPI contract accurate.

Suggested patch
 			response: {
+				400: ErrorResponseSchema,
 			},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
response: {
201: ArtistRoleResponseSchema,
401: ErrorResponseSchema,
},
response: {
201: ArtistRoleResponseSchema,
400: ErrorResponseSchema,
401: ErrorResponseSchema,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/src/handlers/catalog/artistRole/create.ts` around lines 28 - 31,
The response map for the create route in
apps/backend/src/handlers/catalog/artistRole/create.ts is missing a 400 entry
for validation errors; update the response object (the response map used by the
create handler) to include 400: ErrorResponseSchema alongside 201:
ArtistRoleResponseSchema and 401: ErrorResponseSchema so that body validation
failures are documented in the OpenAPI contract.

}
);
34 changes: 34 additions & 0 deletions apps/backend/src/handlers/catalog/artistRole/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Elysia } from "elysia";
import { z } from "zod";
import { ErrorResponseSchema, artistRoleService } from "@cvsa/core";
import { authMiddleware } from "@/middlewares";
import { traceTask } from "@/common/trace";

export const artistRoleDeleteHandler = new Elysia({ name: "artistRoleDeleteHandler" })
.use(authMiddleware)
.delete(
"/artist-role/:id",
async ({ params, set }) => {
await traceTask("artistRoleService.delete", async () => {
return await artistRoleService.delete(params.id);
});
set.status = 204;
return null;
},
{
params: z.object({
id: z.coerce.number().int().positive(),
}),
detail: {
summary: "Delete Artist Role",
description:
"Soft delete an artist role from the catalog. Requires authentication. The role record is marked as deleted but retained in the database for data integrity.",
},
response: {
204: z.null(),
400: ErrorResponseSchema,
401: ErrorResponseSchema,
404: ErrorResponseSchema,
},
}
);
32 changes: 32 additions & 0 deletions apps/backend/src/handlers/catalog/artistRole/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Elysia } from "elysia";
import {
ErrorResponseSchema,
ArtistRoleDetailsResponseSchema,
artistRoleService,
} from "@cvsa/core";
import z from "zod";
import { traceTask } from "@/common/trace";

export const artistRoleDetailsHandler = new Elysia().get(
"/artist-role/:id",
async ({ params, status }) => {
const role = await traceTask("artistRoleService.getDetails", async () => {
return await artistRoleService.getDetails(params.id);
});
return status(200, role);
},
{
detail: {
summary: "Artist Role Details",
description: "Retrieve detailed information about a specific artist role by its ID.",
},
response: {
200: ArtistRoleDetailsResponseSchema,
400: ErrorResponseSchema,
404: ErrorResponseSchema,
},
params: z.object({
id: z.coerce.number().int().positive(),
}),
}
);
13 changes: 13 additions & 0 deletions apps/backend/src/handlers/catalog/artistRole/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Elysia } from "elysia";
import { artistRoleCreateHandler } from "./create";
import { artistRoleUpdateHandler } from "./update";
import { artistRoleDeleteHandler } from "./delete";
import { artistRoleDetailsHandler } from "./get";
import { artistRoleSearchHandler } from "./search";

export const artistRoleHandler = new Elysia({ name: "artistRoleHandler" })
.use(artistRoleDetailsHandler)
.use(artistRoleCreateHandler)
.use(artistRoleUpdateHandler)
.use(artistRoleDeleteHandler)
.use(artistRoleSearchHandler);
25 changes: 25 additions & 0 deletions apps/backend/src/handlers/catalog/artistRole/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Elysia } from "elysia";
import { artistRoleSearchService } from "@cvsa/core";
import z from "zod";
import { traceTask } from "@/common/trace";

// TODO: add corresponding DTO and response schema
export const artistRoleSearchHandler = new Elysia().get(
"/artist-roles",
async ({ query, status }) => {
const result = await traceTask("artistRoleSearchService.search", async () => {
return await artistRoleSearchService.search(query.q || "");
});
return status(200, result);
},
{
detail: {
summary: "Search for Artist Roles",
description:
"Full-text search across artist role names. Returns a list of matching roles ordered by relevance.",
},
query: z.object({
q: z.string().optional(),
}),
}
);
39 changes: 39 additions & 0 deletions apps/backend/src/handlers/catalog/artistRole/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Elysia } from "elysia";
import { z } from "zod";
import {
UpdateArtistRoleRequestSchema,
ErrorResponseSchema,
artistRoleService,
ArtistRoleResponseSchema,
} from "@cvsa/core";
import { authMiddleware } from "@/middlewares";
import { traceTask } from "@/common/trace";

export const artistRoleUpdateHandler = new Elysia({ name: "artistRoleUpdateHandler" })
.use(authMiddleware)
.patch(
"/artist-role/:id",
async ({ params, body, status }) => {
const role = await traceTask("artistRoleService.update", async () => {
return await artistRoleService.update(params.id, body);
});
return status(200, role);
},
{
body: UpdateArtistRoleRequestSchema,
params: z.object({
id: z.coerce.number().int().positive(),
}),
detail: {
summary: "Update Artist Role",
description:
"Update metadata of an existing artist role identified by its ID. Requires authentication.",
},
response: {
200: ArtistRoleResponseSchema,
400: ErrorResponseSchema,
401: ErrorResponseSchema,
404: ErrorResponseSchema,
},
}
);
Loading