Initial attendee management
This commit is contained in:
18
apps/backend/src/modules/groups/features/get-by-id/route.ts
Normal file
18
apps/backend/src/modules/groups/features/get-by-id/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Hono } from 'hono';
|
||||
import { problemJson, resultJson, unauthorizedProblem } from '@/http/problem-details';
|
||||
import { GetGroupById } from './use-case';
|
||||
|
||||
const route = new Hono();
|
||||
|
||||
route.get('/:groupId', async (c) => {
|
||||
const user = c.get('user');
|
||||
if (!user) {
|
||||
return problemJson(c, unauthorizedProblem(c.req.path));
|
||||
}
|
||||
const groupId = c.req.param('groupId');
|
||||
const useCase = new GetGroupById();
|
||||
const result = await useCase.execute(groupId, user.id);
|
||||
return resultJson(c, result);
|
||||
});
|
||||
|
||||
export default route;
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { PrismaClient } from '@generated/prisma/client';
|
||||
import type { GroupDto, ProblemDetails, Result } from '@gruperly/shared';
|
||||
import { err, ok } from '@gruperly/shared';
|
||||
import { noGroupAccessProblem, notFoundResourceProblem } from '@/http/problem-builders';
|
||||
import prisma from '@/lib/prisma';
|
||||
import { type GroupRecord, toGroupDto } from '../../lib';
|
||||
|
||||
type GetGroupByIdDeps = {
|
||||
db?: Pick<PrismaClient, 'group'>;
|
||||
};
|
||||
|
||||
export class GetGroupById {
|
||||
constructor(private readonly deps: GetGroupByIdDeps = {}) {}
|
||||
|
||||
async execute(
|
||||
groupId: string,
|
||||
userId: string,
|
||||
): Promise<Result<GroupDto, ProblemDetails>> {
|
||||
const db = this.deps.db ?? prisma;
|
||||
|
||||
const group = await db.group.findUnique({
|
||||
where: { id: groupId },
|
||||
include: {
|
||||
members: { where: { userId } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
return err(notFoundResourceProblem('Group', groupId));
|
||||
}
|
||||
|
||||
const isOwner = group.createdById === userId;
|
||||
const isMember = group.members.length > 0;
|
||||
if (!isOwner && !isMember) {
|
||||
return err(noGroupAccessProblem());
|
||||
}
|
||||
|
||||
return ok(toGroupDto(group as unknown as GroupRecord));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Hono } from 'hono';
|
||||
import { problemJson, resultJson, unauthorizedProblem } from '@/http/problem-details';
|
||||
import { GetOrCreateInviteToken } from './use-case';
|
||||
|
||||
const route = new Hono();
|
||||
|
||||
route.get('/:groupId/invite-token', async (c) => {
|
||||
const user = c.get('user');
|
||||
if (!user) {
|
||||
return problemJson(c, unauthorizedProblem(c.req.path));
|
||||
}
|
||||
const groupId = c.req.param('groupId');
|
||||
const useCase = new GetOrCreateInviteToken();
|
||||
const result = await useCase.execute(groupId, user.id);
|
||||
return resultJson(c, result);
|
||||
});
|
||||
|
||||
route.post('/:groupId/invite-token', async (c) => {
|
||||
const user = c.get('user');
|
||||
if (!user) {
|
||||
return problemJson(c, unauthorizedProblem(c.req.path));
|
||||
}
|
||||
const groupId = c.req.param('groupId');
|
||||
const regenerate = c.req.query('regenerate') === 'true';
|
||||
const useCase = new GetOrCreateInviteToken();
|
||||
const result = await useCase.execute(groupId, user.id, { regenerate });
|
||||
return resultJson(c, result, { status: 201 });
|
||||
});
|
||||
|
||||
export default route;
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { PrismaClient } from '@generated/prisma/client';
|
||||
import type { InviteTokenResult, ProblemDetails, Result } from '@gruperly/shared';
|
||||
import { err, ok } from '@gruperly/shared';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { noGroupAccessProblem, notFoundResourceProblem } from '@/http/problem-builders';
|
||||
import prisma from '@/lib/prisma';
|
||||
|
||||
type InviteTokenDeps = {
|
||||
db?: Pick<PrismaClient, 'group' | 'groupMember'>;
|
||||
};
|
||||
|
||||
export class GetOrCreateInviteToken {
|
||||
constructor(private readonly deps: InviteTokenDeps = {}) {}
|
||||
|
||||
async execute(
|
||||
groupId: string,
|
||||
userId: string,
|
||||
options?: { regenerate?: boolean },
|
||||
): Promise<Result<InviteTokenResult, ProblemDetails>> {
|
||||
const db = this.deps.db ?? prisma;
|
||||
|
||||
const group = await db.group.findUnique({
|
||||
where: { id: groupId },
|
||||
include: {
|
||||
members: { where: { userId } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!group) {
|
||||
return err(notFoundResourceProblem('Group', groupId));
|
||||
}
|
||||
|
||||
const isOwner = group.createdById === userId;
|
||||
const isMember = group.members.length > 0;
|
||||
if (!isOwner && !isMember) {
|
||||
return err(noGroupAccessProblem());
|
||||
}
|
||||
|
||||
let token = group.inviteToken;
|
||||
if (!token || options?.regenerate) {
|
||||
token = nanoid(12);
|
||||
await db.group.update({
|
||||
where: { id: groupId },
|
||||
data: { inviteToken: token },
|
||||
});
|
||||
}
|
||||
|
||||
const baseUrl = process.env.WEB_URL ?? 'http://localhost:6173';
|
||||
const inviteUrl = `${baseUrl}/join/${token}`;
|
||||
|
||||
return ok({ token, inviteUrl });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type AttendeeQuery, AttendeeQuerySchema } from '@gruperly/shared';
|
||||
import { Hono } from 'hono';
|
||||
import { resultJson } from '@/http/problem-details';
|
||||
import { validate } from '@/http/validate';
|
||||
import { ListAttendees } from '@/modules/attendees/features/get-all/use-case';
|
||||
|
||||
const route = new Hono();
|
||||
|
||||
route.get('/:groupId/attendees', validate.query(AttendeeQuerySchema), async (c) => {
|
||||
const groupId = c.req.param('groupId');
|
||||
const query = c.req.valid('query') as AttendeeQuery;
|
||||
const useCase = new ListAttendees();
|
||||
const result = await useCase.execute({ ...query, groupId });
|
||||
return resultJson(c, result);
|
||||
});
|
||||
|
||||
export default route;
|
||||
@@ -18,6 +18,7 @@ export type GroupRecord = {
|
||||
price: PriceLike | number | null;
|
||||
billingType: BillingType | null;
|
||||
dueDay: number | null;
|
||||
inviteToken?: string | null;
|
||||
};
|
||||
|
||||
export function toGroupDto(record: GroupRecord): GroupDto {
|
||||
@@ -34,6 +35,7 @@ export function toGroupDto(record: GroupRecord): GroupDto {
|
||||
price: record.price == null ? null : Number(record.price.toString()),
|
||||
billingType: record.billingType,
|
||||
dueDay: record.dueDay,
|
||||
inviteToken: record.inviteToken ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { Hono } from 'hono';
|
||||
import bulkCreateAttendeesRoute from '../attendees/features/bulk-create/route';
|
||||
import createAttendeeRoute from '../attendees/features/create/route';
|
||||
import createFromOrganizationRoute from './features/create-from-organization/route';
|
||||
import getAllRoute from './features/get-all/route';
|
||||
import getByIdRoute from './features/get-by-id/route';
|
||||
import inviteTokenRoute from './features/invite-token/route';
|
||||
import listAttendeesRoute from './features/list-attendees/route';
|
||||
|
||||
const routes = new Hono();
|
||||
|
||||
routes.route('/', getAllRoute);
|
||||
routes.route('/from-organization', createFromOrganizationRoute);
|
||||
routes.route('/', inviteTokenRoute);
|
||||
routes.route('/', listAttendeesRoute);
|
||||
routes.route('/', createAttendeeRoute);
|
||||
routes.route('/', bulkCreateAttendeesRoute);
|
||||
routes.route('/', getByIdRoute);
|
||||
|
||||
export default routes;
|
||||
Reference in New Issue
Block a user