Skip to content

Commit 7e56083

Browse files
authored
Merge pull request #1183 from david-roper/add-user-to-session
Add user to session
2 parents 663abd6 + 022e0c0 commit 7e56083

File tree

11 files changed

+85
-12
lines changed

11 files changed

+85
-12
lines changed

apps/api/prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ model User {
202202
basePermissionLevel BasePermissionLevel?
203203
additionalPermissions AuthRule[]
204204
firstName String
205+
sessions Session[]
205206
groupIds String[] @db.ObjectId
206207
groups Group[] @relation(fields: [groupIds], references: [id])
207208
lastName String
@@ -229,6 +230,8 @@ model Session {
229230
instrumentRecords InstrumentRecord[]
230231
subject Subject? @relation(fields: [subjectId], references: [id])
231232
subjectId String
233+
user User? @relation(fields: [userId], references: [id])
234+
userId String?
232235
type SessionType
233236
234237
@@map("SessionModel")

apps/api/src/sessions/dto/create-session.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export class CreateSessionDto {
99
groupId: null | string;
1010
subjectData: CreateSubjectData;
1111
type: SessionType;
12+
userId?: null | string;
1213
}

apps/api/src/sessions/sessions.service.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { accessibleQuery, InjectModel, LoggingService } from '@douglasneuroinformatics/libnest';
2-
import type { Model } from '@douglasneuroinformatics/libnest';
1+
import { accessibleQuery, InjectModel, InjectPrismaClient, LoggingService } from '@douglasneuroinformatics/libnest';
2+
import type { ExtendedPrismaClient, Model } from '@douglasneuroinformatics/libnest';
33
import { Injectable } from '@nestjs/common';
44
import { InternalServerErrorException, NotFoundException } from '@nestjs/common/exceptions';
55
import type { Group } from '@opendatacapture/schemas/group';
66
import type { CreateSessionData } from '@opendatacapture/schemas/session';
77
import type { CreateSubjectData } from '@opendatacapture/schemas/subject';
8-
import type { Prisma, Session, Subject } from '@prisma/client';
8+
import type { Prisma, Session, Subject, User } from '@prisma/client';
99

1010
import type { EntityOperationOptions } from '@/core/types';
1111
import { GroupsService } from '@/groups/groups.service';
@@ -14,6 +14,7 @@ import { SubjectsService } from '@/subjects/subjects.service';
1414
@Injectable()
1515
export class SessionsService {
1616
constructor(
17+
@InjectPrismaClient() private readonly prismaClient: ExtendedPrismaClient,
1718
@InjectModel('Session') private readonly sessionModel: Model<'Session'>,
1819
private readonly groupsService: GroupsService,
1920
private readonly loggingService: LoggingService,
@@ -26,10 +27,20 @@ export class SessionsService {
2627
});
2728
}
2829

29-
async create({ date, groupId, subjectData, type }: CreateSessionData): Promise<Session> {
30+
async create({ date, groupId, subjectData, type, username }: CreateSessionData): Promise<Session> {
3031
this.loggingService.debug({ message: 'Attempting to create session' });
3132
const subject = await this.resolveSubject(subjectData);
3233

34+
let user: null | Omit<User, 'hashedPassword'> = null;
35+
36+
if (username) {
37+
user = await this.prismaClient.user.findFirst({
38+
where: {
39+
username: username
40+
}
41+
});
42+
}
43+
3344
// If the subject is not yet associated with the group, check it exists then append it
3445
let group: Group | null = null;
3546
if (groupId && !subject.groupIds.includes(groupId)) {
@@ -48,7 +59,12 @@ export class SessionsService {
4859
subject: {
4960
connect: { id: subject.id }
5061
},
51-
type
62+
type,
63+
user: user
64+
? {
65+
connect: { id: user.id }
66+
}
67+
: undefined
5268
}
5369
});
5470

apps/api/src/users/users.controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import { UsersService } from './users.service';
1212
export class UsersController {
1313
constructor(private readonly usersService: UsersService) {}
1414

15+
@ApiOperation({ summary: 'Get User by Username' })
16+
@Get('/check-username/:username')
17+
@RouteAccess({ action: 'read', subject: 'User' })
18+
checkUsernameExists(@Param('username') username: string, @CurrentUser('ability') ability: AppAbility) {
19+
return this.usersService.checkUsernameExists(username, { ability });
20+
}
21+
1522
@ApiOperation({ summary: 'Create User' })
1623
@Post()
1724
@RouteAccess({ action: 'create', subject: 'User' })

apps/api/src/users/users.service.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ export class UsersService {
1717
private readonly groupsService: GroupsService
1818
) {}
1919

20+
async checkUsernameExists(username: string, { ability }: EntityOperationOptions = {}): Promise<{ success: boolean }> {
21+
const user = await this.userModel.findFirst({
22+
include: { groups: true },
23+
omit: {
24+
hashedPassword: true
25+
},
26+
where: { AND: [accessibleQuery(ability, 'read', 'User'), { username }] }
27+
});
28+
if (!user) {
29+
return { success: false };
30+
}
31+
return { success: true };
32+
}
33+
2034
async count(
2135
filter: NonNullable<Parameters<Model<'User'>['count']>[0]>['where'] = {},
2236
{ ability }: EntityOperationOptions = {}

apps/web/src/components/StartSessionForm/StartSessionForm.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,16 @@ type StartSessionFormProps = {
3535
initialValues?: FormTypes.PartialNullableData<StartSessionFormData>;
3636
onSubmit: (data: CreateSessionData) => Promisable<void>;
3737
readOnly: boolean;
38+
username?: null | string;
3839
};
3940

40-
export const StartSessionForm = ({ currentGroup, initialValues, readOnly, onSubmit }: StartSessionFormProps) => {
41+
export const StartSessionForm = ({
42+
currentGroup,
43+
username,
44+
initialValues,
45+
readOnly,
46+
onSubmit
47+
}: StartSessionFormProps) => {
4148
const { resolvedLanguage, t } = useTranslation();
4249
return (
4350
<Form
@@ -244,6 +251,7 @@ export const StartSessionForm = ({ currentGroup, initialValues, readOnly, onSubm
244251
await onSubmit({
245252
date: sessionDate,
246253
groupId: currentGroup?.id ?? null,
254+
username: username ?? null,
247255
type: sessionType,
248256
subjectData: {
249257
id: subjectId,

apps/web/src/routes/_app/admin/users/create.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import { estimatePasswordStrength } from '@douglasneuroinformatics/libpasswd';
44
import { Form, Heading } from '@douglasneuroinformatics/libui/components';
5-
import { useTranslation } from '@douglasneuroinformatics/libui/hooks';
5+
import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks';
66
import { $BasePermissionLevel, $CreateUserData } from '@opendatacapture/schemas/user';
77
import type { CreateUserData } from '@opendatacapture/schemas/user';
88
import { createFileRoute, useNavigate } from '@tanstack/react-router';
9+
import axios from 'axios';
910
import { z } from 'zod/v4';
1011

1112
import { PageHeader } from '@/components/PageHeader';
@@ -17,10 +18,24 @@ const RouteComponent = () => {
1718
const navigate = useNavigate();
1819
const groupsQuery = useGroupsQuery();
1920
const createUserMutation = useCreateUserMutation();
21+
const notification = useNotificationsStore();
2022

21-
const handleSubmit = (data: CreateUserData) => {
22-
createUserMutation.mutate({ data });
23-
void navigate({ to: '..' });
23+
const handleSubmit = async (data: CreateUserData) => {
24+
// check if username exists
25+
const existingUsername = await axios.get<{ success: boolean }>(
26+
`/v1/users/check-username/${encodeURIComponent(data.username)}`
27+
);
28+
29+
if (existingUsername.data.success === true) {
30+
notification.addNotification({
31+
type: 'error',
32+
message: t('common.usernameExists')
33+
});
34+
} else {
35+
void createUserMutation.mutateAsync({ data });
36+
37+
void navigate({ to: '..' });
38+
}
2439
};
2540

2641
return (

apps/web/src/routes/_app/session/start-session.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const RouteComponent = () => {
1515
const currentGroup = useAppStore((store) => store.currentGroup);
1616
const currentSession = useAppStore((store) => store.currentSession);
1717
const startSession = useAppStore((store) => store.startSession);
18+
const currentUser = useAppStore((store) => store.currentUser);
1819
const location = useLocation();
1920
const defaultInitialValues = {
2021
sessionType: 'IN_PERSON',
@@ -44,6 +45,7 @@ const RouteComponent = () => {
4445
currentGroup={currentGroup}
4546
initialValues={initialValues}
4647
readOnly={currentSession !== null || createSessionMutation.isPending}
48+
username={currentUser?.username}
4749
onSubmit={async (formData) => {
4850
const session = await createSessionMutation.mutateAsync(formData);
4951
startSession({ ...session, type: formData.type });

apps/web/src/translations/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,5 +150,9 @@
150150
"username": {
151151
"en": "Username",
152152
"fr": "Nom d'utilisateur"
153+
},
154+
"usernameExists": {
155+
"en": "Username already exists",
156+
"fr": "Le nom d'utilisateur existe déjà"
153157
}
154158
}

packages/schemas/src/session/session.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ export const $Session = $BaseModel.extend({
1212
groupId: z.string().nullable(),
1313
subject: $Subject,
1414
subjectId: z.string(),
15-
type: $SessionType
15+
type: $SessionType,
16+
userId: z.string().nullish()
1617
});
1718

1819
export type CreateSessionData = z.infer<typeof $CreateSessionData>;
1920
export const $CreateSessionData = z.object({
2021
date: z.coerce.date(),
2122
groupId: z.string().nullable(),
2223
subjectData: $CreateSubjectData,
23-
type: $SessionType
24+
type: $SessionType,
25+
username: z.string().nullish()
2426
});

0 commit comments

Comments
 (0)