-
Notifications
You must be signed in to change notification settings - Fork 1
feat(artistRole): CRUD API for artistRole #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7d3d45e
dcaff84
7b1783a
382699d
f744c09
a96af0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep anti-abuse throttling on signup.
🤖 Prompt for AI Agents |
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Document 400 validation errors in the create route response map. This route validates Suggested patch response: {
+ 400: ErrorResponseSchema,
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| 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, | ||
| }, | ||
| } | ||
| ); |
| 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(), | ||
| }), | ||
| } | ||
| ); |
| 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); |
| 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(), | ||
| }), | ||
| } | ||
| ); |
| 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, | ||
| }, | ||
| } | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reintroduce throttling on the login endpoint.
POST /sessionis 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