Compare commits

..

91 commits

Author SHA1 Message Date
a8cc166f41 fix: add suspence boundary around event form
All checks were successful
container-scan / Container Scan (pull_request) Successful in 5m0s
docker-build / docker (pull_request) Successful in 5m32s
2025-06-24 20:54:17 +02:00
a529b939c4 feat: enhance EventForm to handle URL parameters for creating events 2025-06-24 20:54:17 +02:00
10080b8513 feat: add user image to ParticipantListEntry component 2025-06-24 20:54:16 +02:00
44a75ff12e style: make events page a little bit prettier 2025-06-24 20:54:16 +02:00
de71e725e4 feat: add Events page and EventListEntry component for displaying events 2025-06-24 20:54:15 +02:00
d2c10f6a0a refactor: rename event folder to events 2025-06-24 20:54:15 +02:00
8f21ab1d68 fix: update parameter typing in Page function to use Promise 2025-06-24 20:54:14 +02:00
73004dc2d8 feat: light mode user icon 2025-06-24 20:54:14 +02:00
83b4b4476a refactor: rename EditEventPage function to Page 2025-06-24 20:54:13 +02:00
382bece410 refactor: simplify parameter typing in EditEventPage function 2025-06-24 20:54:13 +02:00
b464e6b511 fix: correct parameter extraction in EditEventPage function 2025-06-24 20:54:12 +02:00
911ec29234 fix: format 2025-06-24 20:54:12 +02:00
9ef984eccb fix: update default background color and adjust layout for ToastInner component 2025-06-24 20:54:12 +02:00
49111aaa3f feat: enhance event management with participant selection and user search functionality 2025-06-24 20:54:11 +02:00
33a07f073b feat: add Command and Dialog components with necessary dependencies 2025-06-24 20:54:11 +02:00
6a4bbae300 feat: add default user icon and participant list entry component 2025-06-24 20:54:10 +02:00
b135a61130 refactor: move NewEvent page for improved site structure 2025-06-24 20:54:10 +02:00
c0ff8e87bb feat: create edit Event Page and add closeOnAction prop for Toaster 2025-06-24 20:54:10 +02:00
a469948da5 feat: implement responsive expansion for Toaster component 2025-06-24 20:54:10 +02:00
d97d2c84d2 fix(toast-inner): remove unnecessary router import 2025-06-24 20:54:09 +02:00
7f419afe47 feat(event-page): create ShowEvent component to display event details 2025-06-24 20:54:09 +02:00
9a187788c4 feat(event-form): replace toast notification with custom ToastInner component 2025-06-24 20:54:09 +02:00
71315cbb5b fix(redirect-button): update import path for Button component 2025-06-24 20:54:08 +02:00
49386afc07 fix(label): correct size class for large label from 'text-lg' to 'text-xl' 2025-06-24 20:54:08 +02:00
0caddb59d8 feat: integrate sonner for toast notifications and add ToastInner component 2025-06-24 20:54:08 +02:00
70a819f525 refactor: update import paths 2025-06-24 20:54:08 +02:00
05649e3c11 fix: button component 2025-06-24 20:54:07 +02:00
4476ee6eb9 feat(event-form): fix view of created_at and updated_at on edit and implement redirect for save and cancel buttons 2025-06-24 20:54:07 +02:00
05f56a2186 refactor: remove Button component and its associated styles 2025-06-24 20:54:07 +02:00
d3dfc3dc0d feat: update button variant usage in TimePicker and Calendar components and add Date-Range to Calendar 2025-06-24 20:54:07 +02:00
68d36c62ce fix: update import paths for API hooks in event form 2025-06-24 20:54:06 +02:00
53973c114e fix: update import paths for useGetApiUserMe and related components in event form 2025-06-24 20:54:06 +02:00
7ee8df3732 feat(event-form): update EventForm to support editing events with eventId and pre-fill form fields 2025-06-24 20:54:06 +02:00
8369a92520 feat: add ParticipantListEntry component and integrate draft into event form 2025-06-24 20:54:06 +02:00
f5450d9b4f fix: fix conflicts and changes after rebasing 2025-06-24 20:54:05 +02:00
a6694c4f7e feat: create db-entry on event-form-submit 2025-06-24 20:54:05 +02:00
25ebb4bb3b fix(logo): update warning message for missing width or height props 2025-06-24 20:54:05 +02:00
aebbfb9f5d fix: rename Labeled-Input size prop to variantSize 2025-06-24 20:54:05 +02:00
4772425abd fix(yarn.lock): regenerate yarn.lock 2025-06-24 20:54:04 +02:00
38401d4eba feat(event-form): add type prop to EventForm for create/edit functionality 2025-06-24 20:54:04 +02:00
af18024baf feat(labeled-input): add Big labeled input field 2025-06-24 20:54:04 +02:00
93a75b9214 feat(event-form): replace text elements with Label components for consistency 2025-06-24 20:54:04 +02:00
6eae281fd0 fix(calendar): correct background color variable in dropdown class 2025-06-24 20:54:03 +02:00
6c2c09f9e2 feat(label): add size prop to Label component for responsive text sizing 2025-06-24 20:54:03 +02:00
43e24222cc feat(event-form): enhance event form layout with TimePicker and input fields 2025-06-24 20:54:03 +02:00
f326bd8e1a feat: implement TimePicker component with date and time selection 2025-06-24 20:54:03 +02:00
29b3490132 feat: add calendar and popover components with button variants 2025-06-24 20:54:02 +02:00
3de329b157 feat: create basic layout for new new event page 2025-06-24 20:54:02 +02:00
d7da7f85cd feat(styles): define typography styles for headings and paragraphs 2025-06-24 20:54:02 +02:00
2484546be3 fix(card): update shadow style for improved visual effect 2025-06-24 20:54:01 +02:00
47f08a9bf7 feat(api): add swagger docs, /api/user/me GET and PUT endpoints 2025-06-24 20:54:01 +02:00
d42294fb0c style: format code 2025-06-24 20:54:01 +02:00
fe2d1ff0f3 feat(api): add react-query client generation and redesign api responses 2025-06-24 20:54:01 +02:00
378991e582 feat(api): add swagger docs, /api/user/me GET and PUT endpoints 2025-06-24 20:54:00 +02:00
b8ad6891e3 refactor(validation): restucture api input and output validation 2025-06-24 20:53:26 +02:00
8207230886 feat(api): add user search endpoint and normalize response data and validation 2025-06-24 20:50:40 +02:00
b5dfe9a4e3 feat(api): add tags to event and user routes for better categorization 2025-06-24 20:47:29 +02:00
4b0f51f444 feat(api): add participant management endpoints and update event response structure 2025-06-24 20:46:01 +02:00
e426e0f861 style: format code 2025-06-24 20:45:21 +02:00
2d3a6f7d6c feat(api): add endpoint for event creation and listing of all events 2025-06-24 20:44:23 +02:00
2e2b74b282 feat(api): add react-query client generation and redesign api responses 2025-06-24 20:44:12 +02:00
f245b4156f feat(api): add swagger docs, /api/user/me GET and PUT endpoints 2025-06-24 20:40:28 +02:00
d62e954348 chore(deps): update dependency @types/node to v22.15.33
All checks were successful
container-scan / Container Scan (push) Successful in 4m19s
docker-build / docker (push) Successful in 1m19s
2025-06-24 17:01:52 +00:00
2889424bfb fix(deps): update dependency next to v15.4.0-canary.94
All checks were successful
container-scan / Container Scan (push) Successful in 4m11s
docker-build / docker (push) Successful in 1m23s
2025-06-24 00:01:37 +00:00
3ee0dcf950 fix(deps): update dependency next to v15.4.0-canary.93
All checks were successful
container-scan / Container Scan (push) Successful in 3m35s
docker-build / docker (push) Successful in 1m4s
2025-06-23 11:01:37 +00:00
c98a72f2f1 Merge pull request 'feat(api): implement missing user update and delete endpoints' (#102)
All checks were successful
container-scan / Container Scan (push) Successful in 4m58s
docker-build / docker (push) Successful in 7m17s
Reviewed-on: #102
Reviewed-by: Maximilian Liebmann <lima@noreply.git.dominikstahl.dev>
2025-06-23 09:37:13 +00:00
29f2a01ac6
style: format code
All checks were successful
container-scan / Container Scan (pull_request) Successful in 3m10s
docker-build / docker (pull_request) Successful in 7m25s
2025-06-23 10:45:56 +02:00
4cf5ce26ff
feat(api): implement DELETE method for /api/user/me endpoint
Some checks failed
container-scan / Container Scan (pull_request) Failing after 32s
docker-build / docker (pull_request) Successful in 7m22s
2025-06-23 10:44:26 +02:00
16b878a2e9
feat(api): stricter user data api types checking 2025-06-23 10:40:28 +02:00
280fa57e45
feat(api): implement /api/user/me/password endpoint
Some checks failed
container-scan / Container Scan (pull_request) Failing after 33s
docker-build / docker (pull_request) Failing after 5m11s
add an endpoint to allow the user to change his password
2025-06-23 10:00:19 +02:00
be1502a4ac fix(deps): update dependency next to v15.4.0-canary.92
Some checks failed
container-scan / Container Scan (push) Failing after 4m35s
docker-build / docker (push) Successful in 1m30s
2025-06-23 00:00:52 +00:00
f240bf525d fix(deps): update dependency @auth/prisma-adapter to v2.10.0
All checks were successful
container-scan / Container Scan (push) Successful in 12m38s
docker-build / docker (push) Successful in 2m20s
2025-06-22 14:02:45 +00:00
525b8597f2 fix(deps): update dependency next-auth to v5.0.0-beta.29
All checks were successful
container-scan / Container Scan (push) Successful in 11m8s
docker-build / docker (push) Successful in 13m15s
2025-06-22 13:01:47 +00:00
0da8e35b9b fix(deps): update dependency next to v15.4.0-canary.91
All checks were successful
container-scan / Container Scan (push) Successful in 11m26s
docker-build / docker (push) Successful in 2m9s
2025-06-22 00:03:20 +00:00
cd1ad5dbc4 fix(deps): update dependency @tanstack/react-query to v5.81.2
All checks were successful
container-scan / Container Scan (push) Successful in 12m24s
docker-build / docker (push) Successful in 1m59s
2025-06-21 23:00:48 +00:00
5fbd7ac091 fix(deps): update dependency @tanstack/react-query to v5.81.1
All checks were successful
container-scan / Container Scan (push) Successful in 11m46s
docker-build / docker (push) Successful in 2m9s
2025-06-21 20:01:54 +00:00
5e6feb39eb fix(deps): update dependency @tanstack/react-query to v5.81.0
All checks were successful
container-scan / Container Scan (push) Successful in 10m45s
docker-build / docker (push) Successful in 1m50s
2025-06-21 13:00:48 +00:00
b652499788 fix(deps): update dependency next to v15.4.0-canary.90
All checks were successful
container-scan / Container Scan (push) Successful in 12m6s
docker-build / docker (push) Successful in 1m48s
2025-06-21 02:01:51 +00:00
96ff00f120 chore(deps): update dependency prisma to v6.10.1
All checks were successful
container-scan / Container Scan (push) Successful in 11m1s
docker-build / docker (push) Successful in 12m29s
2025-06-21 00:01:44 +00:00
58cf178968 fix(deps): update dependency next to v15.4.0-canary.89
All checks were successful
container-scan / Container Scan (push) Successful in 9m57s
docker-build / docker (push) Successful in 12m5s
2025-06-20 23:01:56 +00:00
360f0788dd Merge pull request 'feat: core api endpoints' (#95)
All checks were successful
container-scan / Container Scan (push) Successful in 4m0s
docker-build / docker (push) Successful in 11m56s
Reviewed-on: #95
Reviewed-by: micha <MichaBokelmann@web.de>
2025-06-20 22:13:01 +00:00
445a15ccc7
feat(api): add username to homepage
All checks were successful
container-scan / Container Scan (pull_request) Successful in 8m8s
docker-build / docker (pull_request) Successful in 8m33s
example for the api usage
2025-06-20 14:00:13 +02:00
40d13101a3
feat(api): implement /api/event/[eventID]/participant/[user] endpoint 2025-06-20 14:00:12 +02:00
68cafccec7
feat(api): implement /api/event/[eventID]/participant endpoint 2025-06-20 14:00:11 +02:00
f5a5704be3
feat(api): implement /api/event/[eventID] endpoint 2025-06-20 14:00:10 +02:00
50d915854f
feat(api): implement /api/event endpoint 2025-06-20 14:00:09 +02:00
b10b374b84
feat(api): implement /api/search/user endpoint 2025-06-20 14:00:08 +02:00
c71de4a14c
feat(api): implement /api/user/[user]/calendar endpoint 2025-06-20 14:00:07 +02:00
eb04c276ab
feat(api): implement /api/user/[user] endpoint 2025-06-20 13:59:45 +02:00
3e890d4363
feat(api): implement /api/user/me endpoint 2025-06-20 13:59:44 +02:00
87dc6162f4
feat(api): upgrade zod to v4 and implement api docs and client generation 2025-06-20 13:59:43 +02:00
13 changed files with 4668 additions and 342 deletions

View file

@ -45,7 +45,7 @@
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.511.0",
"next": "15.4.0-canary.85",
"next": "15.4.0-canary.94",
"next-auth": "^5.0.0-beta.25",
"next-swagger-doc": "^0.4.1",
"next-themes": "^0.4.6",
@ -61,22 +61,22 @@
"devDependencies": {
"@eslint/eslintrc": "3.3.1",
"@tailwindcss/postcss": "4.1.10",
"@types/node": "22.15.32",
"@types/node": "22.15.33",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/swagger-ui-react": "^5",
"@types/webpack-env": "^1.18.8",
"@types/swagger-ui-react": "5",
"@types/webpack-env": "1.18.8",
"dotenv-cli": "8.0.0",
"eslint": "9.29.0",
"eslint-config-next": "15.3.4",
"eslint-config-prettier": "10.1.5",
"orval": "^7.10.0",
"orval": "7.10.0",
"postcss": "8.5.6",
"prettier": "3.5.3",
"prisma": "6.9.0",
"prisma": "6.10.1",
"tailwindcss": "4.1.10",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"tw-animate-css": "1.3.4",
"typescript": "^5.8.3"
},

View file

@ -0,0 +1,212 @@
import { auth } from '@/auth';
import { prisma } from '@/prisma';
import {
returnZodTypeCheckedResponse,
userAuthenticated,
} from '@/lib/apiHelpers';
import {
userCalendarQuerySchema,
UserCalendarResponseSchema,
UserCalendarSchema,
} from './validation';
import {
ErrorResponseSchema,
ZodErrorResponseSchema,
} from '@/app/api/validation';
import { z } from 'zod/v4';
export const GET = auth(async function GET(req, { params }) {
const authCheck = userAuthenticated(req);
if (!authCheck.continue)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
authCheck.response,
authCheck.metadata,
);
const dataRaw = Object.fromEntries(new URL(req.url).searchParams);
const data = await userCalendarQuerySchema.safeParseAsync(dataRaw);
if (!data.success)
return returnZodTypeCheckedResponse(
ZodErrorResponseSchema,
{
success: false,
message: 'Invalid request data',
errors: data.error.issues,
},
{ status: 400 },
);
const { end, start } = data.data;
const requestUserId = authCheck.user.id;
const requestedUserId = (await params).user;
const requestedUser = await prisma.user.findFirst({
where: {
id: requestedUserId,
},
select: {
meetingParts: {
where: {
meeting: {
start_time: {
lte: end,
},
end_time: {
gte: start,
},
},
},
orderBy: {
meeting: {
start_time: 'asc',
},
},
select: {
meeting: {
select: {
id: true,
title: true,
description: true,
start_time: true,
end_time: true,
status: true,
location: true,
created_at: true,
updated_at: true,
organizer_id: true,
participants: {
select: {
user: {
select: {
id: true,
},
},
},
},
},
},
},
},
meetingsOrg: {
where: {
start_time: {
lte: end,
},
end_time: {
gte: start,
},
},
orderBy: {
start_time: 'asc',
},
select: {
id: true,
title: true,
description: true,
start_time: true,
end_time: true,
status: true,
location: true,
created_at: true,
updated_at: true,
organizer_id: true,
participants: {
select: {
user: {
select: {
id: true,
},
},
},
},
},
},
blockedSlots: {
where: {
start_time: {
lte: end,
},
end_time: {
gte: start,
},
},
orderBy: {
start_time: 'asc',
},
select: {
id: requestUserId === requestedUserId ? true : false,
reason: requestUserId === requestedUserId ? true : false,
start_time: true,
end_time: true,
is_recurring: requestUserId === requestedUserId ? true : false,
recurrence_end_date: requestUserId === requestedUserId ? true : false,
rrule: requestUserId === requestedUserId ? true : false,
created_at: requestUserId === requestedUserId ? true : false,
updated_at: requestUserId === requestedUserId ? true : false,
},
},
},
});
if (!requestedUser)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
{ success: false, message: 'User not found' },
{ status: 404 },
);
const calendar: z.input<typeof UserCalendarSchema> = [];
for (const event of requestedUser.meetingParts) {
if (
event.meeting.participants.some((p) => p.user.id === requestUserId) ||
event.meeting.organizer_id === requestUserId
) {
calendar.push({ ...event.meeting, type: 'event' });
} else {
calendar.push({
start_time: event.meeting.start_time,
end_time: event.meeting.end_time,
type: 'blocked_private',
});
}
}
for (const event of requestedUser.meetingsOrg) {
if (
event.participants.some((p) => p.user.id === requestUserId) ||
event.organizer_id === requestUserId
) {
calendar.push({ ...event, type: 'event' });
} else {
calendar.push({
start_time: event.start_time,
end_time: event.end_time,
type: 'blocked_private',
});
}
}
for (const slot of requestedUser.blockedSlots) {
calendar.push({
start_time: slot.start_time,
end_time: slot.end_time,
id: slot.id,
reason: slot.reason,
is_recurring: slot.is_recurring,
recurrence_end_date: slot.recurrence_end_date,
rrule: slot.rrule,
created_at: slot.created_at,
updated_at: slot.updated_at,
type:
requestUserId === requestedUserId ? 'blocked_owned' : 'blocked_private',
});
}
return returnZodTypeCheckedResponse(UserCalendarResponseSchema, {
success: true,
calendar,
});
});

View file

@ -0,0 +1,37 @@
import {
userCalendarQuerySchema,
UserCalendarResponseSchema,
} from './validation';
import {
notAuthenticatedResponse,
userNotFoundResponse,
} from '@/lib/defaultApiResponses';
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import zod from 'zod/v4';
import { UserIdParamSchema } from '@/app/api/validation';
export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
registry.registerPath({
method: 'get',
path: '/api/user/{user}/calendar',
request: {
params: zod.object({
user: UserIdParamSchema,
}),
query: userCalendarQuerySchema,
},
responses: {
200: {
description: 'User calendar retrieved successfully.',
content: {
'application/json': {
schema: UserCalendarResponseSchema,
},
},
},
...notAuthenticatedResponse,
...userNotFoundResponse,
},
tags: ['User'],
});
}

View file

@ -0,0 +1,99 @@
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import zod from 'zod/v4';
import {
eventEndTimeSchema,
EventSchema,
eventStartTimeSchema,
} from '@/app/api/event/validation';
extendZodWithOpenApi(zod);
export const BlockedSlotSchema = zod
.object({
start_time: eventStartTimeSchema,
end_time: eventEndTimeSchema,
type: zod.literal('blocked_private'),
})
.openapi('BlockedSlotSchema', {
description: 'Blocked time slot in the user calendar',
});
export const OwnedBlockedSlotSchema = BlockedSlotSchema.extend({
id: zod.string(),
reason: zod.string().nullish(),
is_recurring: zod.boolean().default(false),
recurrence_end_date: zod.date().nullish(),
rrule: zod.string().nullish(),
created_at: zod.date().nullish(),
updated_at: zod.date().nullish(),
type: zod.literal('blocked_owned'),
}).openapi('OwnedBlockedSlotSchema', {
description: 'Blocked slot owned by the user',
});
export const VisibleSlotSchema = EventSchema.omit({
organizer: true,
participants: true,
})
.extend({
type: zod.literal('event'),
})
.openapi('VisibleSlotSchema', {
description: 'Visible time slot in the user calendar',
});
export const UserCalendarSchema = zod
.array(VisibleSlotSchema.or(BlockedSlotSchema).or(OwnedBlockedSlotSchema))
.openapi('UserCalendarSchema', {
description: 'Array of events in the user calendar',
});
export const UserCalendarResponseSchema = zod.object({
success: zod.boolean().default(true),
calendar: UserCalendarSchema,
});
export const userCalendarQuerySchema = zod
.object({
start: zod.iso
.datetime()
.optional()
.transform((val) => {
if (val) return new Date(val);
const now = new Date();
const startOfWeek = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() - now.getDay(),
);
return startOfWeek;
}),
end: zod.iso
.datetime()
.optional()
.transform((val) => {
if (val) return new Date(val);
const now = new Date();
const endOfWeek = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() + (6 - now.getDay()),
);
return endOfWeek;
}),
})
.openapi('UserCalendarQuerySchema', {
description: 'Query parameters for filtering the user calendar',
properties: {
start: {
type: 'string',
format: 'date-time',
description: 'Start date for filtering the calendar events',
},
end: {
type: 'string',
format: 'date-time',
description: 'End date for filtering the calendar events',
},
},
});

View file

@ -0,0 +1,122 @@
import { auth } from '@/auth';
import { prisma } from '@/prisma';
import { updateUserPasswordServerSchema } from '../validation';
import {
returnZodTypeCheckedResponse,
userAuthenticated,
} from '@/lib/apiHelpers';
import { FullUserResponseSchema } from '../../validation';
import {
ErrorResponseSchema,
ZodErrorResponseSchema,
} from '@/app/api/validation';
import bcrypt from 'bcryptjs';
export const PATCH = auth(async function PATCH(req) {
const authCheck = userAuthenticated(req);
if (!authCheck.continue)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
authCheck.response,
authCheck.metadata,
);
const body = await req.json();
const parsedBody = updateUserPasswordServerSchema.safeParse(body);
if (!parsedBody.success)
return returnZodTypeCheckedResponse(
ZodErrorResponseSchema,
{
success: false,
message: 'Invalid request data',
errors: parsedBody.error.issues,
},
{ status: 400 },
);
const { current_password, new_password } = parsedBody.data;
const dbUser = await prisma.user.findUnique({
where: {
id: authCheck.user.id,
},
include: {
accounts: true,
},
});
if (!dbUser)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
{
success: false,
message: 'User not found',
},
{ status: 404 },
);
if (!dbUser.password_hash)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
{
success: false,
message: 'User does not have a password set',
},
{ status: 400 },
);
if (
dbUser.accounts.length === 0 ||
dbUser.accounts[0].provider !== 'credentials'
)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
{
success: false,
message: 'Credentials login is not enabled for this user',
},
{ status: 400 },
);
const isCurrentPasswordValid = await bcrypt.compare(
current_password,
dbUser.password_hash || '',
);
if (!isCurrentPasswordValid)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
{
success: false,
message: 'Current password is incorrect',
},
{ status: 401 },
);
const hashedNewPassword = await bcrypt.hash(new_password, 10);
const updatedUser = await prisma.user.update({
where: {
id: dbUser.id,
},
data: {
password_hash: hashedNewPassword,
},
select: {
id: true,
name: true,
first_name: true,
last_name: true,
email: true,
image: true,
timezone: true,
created_at: true,
updated_at: true,
},
});
return returnZodTypeCheckedResponse(FullUserResponseSchema, {
success: true,
user: updatedUser,
});
});

View file

@ -0,0 +1,43 @@
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { FullUserResponseSchema } from '../../validation';
import { updateUserPasswordServerSchema } from '../validation';
import {
invalidRequestDataResponse,
notAuthenticatedResponse,
serverReturnedDataValidationErrorResponse,
userNotFoundResponse,
} from '@/lib/defaultApiResponses';
export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
registry.registerPath({
method: 'patch',
path: '/api/user/me/password',
description: 'Update the password of the currently authenticated user',
request: {
body: {
description: 'User password update request body',
required: true,
content: {
'application/json': {
schema: updateUserPasswordServerSchema,
},
},
},
},
responses: {
200: {
description: 'User information updated successfully',
content: {
'application/json': {
schema: FullUserResponseSchema,
},
},
},
...invalidRequestDataResponse,
...notAuthenticatedResponse,
...userNotFoundResponse,
...serverReturnedDataValidationErrorResponse,
},
tags: ['User'],
});
}

View file

@ -8,6 +8,7 @@ import {
import { FullUserResponseSchema } from '../validation';
import {
ErrorResponseSchema,
SuccessResponseSchema,
ZodErrorResponseSchema,
} from '@/app/api/validation';
@ -117,3 +118,43 @@ export const PATCH = auth(async function PATCH(req) {
{ status: 200 },
);
});
export const DELETE = auth(async function DELETE(req) {
const authCheck = userAuthenticated(req);
if (!authCheck.continue)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
authCheck.response,
authCheck.metadata,
);
const dbUser = await prisma.user.findUnique({
where: {
id: authCheck.user.id,
},
});
if (!dbUser)
return returnZodTypeCheckedResponse(
ErrorResponseSchema,
{
success: false,
message: 'User not found',
},
{ status: 404 },
);
await prisma.user.delete({
where: {
id: authCheck.user.id,
},
});
return returnZodTypeCheckedResponse(
SuccessResponseSchema,
{
success: true,
message: 'User deleted successfully',
},
{ status: 200 },
);
});

View file

@ -7,6 +7,7 @@ import {
serverReturnedDataValidationErrorResponse,
userNotFoundResponse,
} from '@/lib/defaultApiResponses';
import { SuccessResponseSchema } from '../../validation';
export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
registry.registerPath({
@ -60,4 +61,24 @@ export default function registerSwaggerPaths(registry: OpenAPIRegistry) {
},
tags: ['User'],
});
registry.registerPath({
method: 'delete',
path: '/api/user/me',
description: 'Delete the currently authenticated user',
responses: {
200: {
description: 'User deleted successfully',
content: {
'application/json': {
schema: SuccessResponseSchema,
},
},
},
...notAuthenticatedResponse,
...userNotFoundResponse,
...serverReturnedDataValidationErrorResponse,
},
tags: ['User'],
});
}

View file

@ -4,6 +4,8 @@ import {
lastNameSchema,
newUserEmailServerSchema,
newUserNameServerSchema,
passwordSchema,
timezoneSchema,
} from '@/app/api/user/validation';
// ----------------------------------------
@ -16,6 +18,16 @@ export const updateUserServerSchema = zod.object({
first_name: firstNameSchema.optional(),
last_name: lastNameSchema.optional(),
email: newUserEmailServerSchema.optional(),
image: zod.string().optional(),
timezone: zod.string().optional(),
image: zod.url().optional(),
timezone: timezoneSchema.optional(),
});
export const updateUserPasswordServerSchema = zod
.object({
current_password: zod.string().min(1, 'Current password is required'),
new_password: passwordSchema,
confirm_new_password: passwordSchema,
})
.refine((data) => data.new_password === data.confirm_new_password, {
message: 'New password and confirm new password must match',
});

View file

@ -1,6 +1,7 @@
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { prisma } from '@/prisma';
import zod from 'zod/v4';
import { allTimeZones } from '@/lib/timezones';
extendZodWithOpenApi(zod);
@ -107,6 +108,15 @@ export const passwordSchema = zod
'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character',
);
// ----------------------------------------
//
// Timezone Validation
//
// ----------------------------------------
export const timezoneSchema = zod.enum(allTimeZones).openapi('Timezone', {
description: 'Valid timezone from the list of supported timezones',
});
// ----------------------------------------
//
// User Schema Validation (for API responses)
@ -119,8 +129,11 @@ export const FullUserSchema = zod
first_name: zod.string().nullish(),
last_name: zod.string().nullish(),
email: zod.email(),
image: zod.string().nullish(),
timezone: zod.string(),
image: zod.url().nullish(),
timezone: zod
.string()
.refine((i) => (allTimeZones as string[]).includes(i))
.nullish(),
created_at: zod.date(),
updated_at: zod.date(),
})

3717
src/lib/timezones.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,6 @@ export { auth as middleware } from '@/auth';
export const config = {
matcher: [
'/((?!api|_next/static|api-doc|_next/image|site\.webmanifest|web-app-manifest-(?:192x192|512x512)\.png|apple-touch-icon.png|favicon(?:-(?:dark|light))?\.(?:png|svg|ico)|fonts).*)',
'/((?!api|_next/static|api-doc|api-doc|_next/image|site\.webmanifest|web-app-manifest-(?:192x192|512x512)\.png|apple-touch-icon.png|favicon(?:-(?:dark|light))?\.(?:png|svg|ico)|fonts).*)',
],
};

667
yarn.lock

File diff suppressed because it is too large Load diff