Skip to content

Commit e31bb22

Browse files
committed
feat: upsert user by oidc
1 parent 9cbcd17 commit e31bb22

14 files changed

Lines changed: 277 additions & 59 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
DO
3+
$$
4+
BEGIN
5+
IF register_patch('AddUniqueConstraintToOidcSub.sql', 'yoganandanpandiyan', 'Unique constraint', '2025-10-13') THEN
6+
BEGIN
7+
ALTER TABLE users ADD CONSTRAINT unique_oidc_sub UNIQUE (oidc_sub);
8+
END;
9+
END IF;
10+
END;
11+
$$
12+
LANGUAGE plpgsql;

apps/backend/src/datasources/UserDataSource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export interface UserDataSource {
6868
oauth_issuer: string,
6969
gender: string,
7070
birthdate: Date,
71-
institution: number,
71+
institution: number | undefined,
7272
department: string,
7373
position: string,
7474
email: string,

apps/backend/src/datasources/mockups/UserDataSource.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const basicDummyUser = new BasicUserDetails(
2828
false,
2929
'test@email.com',
3030
'',
31+
'',
3132
''
3233
);
3334

@@ -43,6 +44,7 @@ export const basicDummyUserNotOnProposal = new BasicUserDetails(
4344
false,
4445
'test@email.com',
4546
'',
47+
'',
4648
''
4749
);
4850

@@ -300,6 +302,7 @@ export class UserDataSourceMock implements UserDataSource {
300302
false,
301303
'test@email.com',
302304
'',
305+
'',
303306
''
304307
);
305308
}

apps/backend/src/datasources/postgres/UserDataSource.ts

Lines changed: 72 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -90,31 +90,40 @@ export default class PostgresUserDataSource implements UserDataSource {
9090
oidcSub,
9191
oauthRefreshToken,
9292
oauthIssuer,
93+
username,
9394
} = user;
9495

95-
const [userRecord]: UserRecord[] = await database
96-
.update({
97-
firstname,
98-
user_title,
99-
lastname,
100-
preferredname,
101-
gender,
102-
birthdate,
103-
institution_id: institutionId,
104-
department,
105-
position,
106-
email,
107-
telephone,
108-
placeholder,
109-
oidc_sub: oidcSub,
110-
oauth_refresh_token: oauthRefreshToken,
111-
oauth_issuer: oauthIssuer,
112-
})
113-
.from('users')
114-
.where('user_id', user.id)
115-
.returning(['*']);
116-
117-
return createUserObject(userRecord);
96+
try {
97+
const [userRecord]: UserRecord[] = await database
98+
.update({
99+
firstname,
100+
user_title,
101+
lastname,
102+
preferredname,
103+
gender,
104+
birthdate,
105+
institution_id: institutionId,
106+
department,
107+
position,
108+
email,
109+
telephone,
110+
placeholder,
111+
oidc_sub: oidcSub,
112+
oauth_refresh_token: oauthRefreshToken,
113+
oauth_issuer: oauthIssuer,
114+
username,
115+
})
116+
.from('users')
117+
.where('user_id', user.id)
118+
.returning(['*']);
119+
120+
return createUserObject(userRecord);
121+
} catch (error) {
122+
if (error instanceof Error && 'code' in error && error.code === '23505') {
123+
throw new GraphQLError('User already exists');
124+
}
125+
throw new GraphQLError('Could not create user. Check your Inputs.');
126+
}
118127
}
119128
async updateUserByOidcSub(
120129
args: UpdateUserByOidcSubArgs
@@ -135,34 +144,40 @@ export default class PostgresUserDataSource implements UserDataSource {
135144
oauthRefreshToken,
136145
oauthIssuer,
137146
} = args;
138-
139-
const [userRecord]: UserRecord[] = await database
140-
.update({
141-
firstname,
142-
user_title,
143-
lastname,
144-
preferredname,
145-
gender,
146-
birthdate,
147-
institution_id: institutionId,
148-
department,
149-
position,
150-
email,
151-
telephone,
152-
placeholder,
153-
oauth_refresh_token: oauthRefreshToken,
154-
oauth_issuer: oauthIssuer,
155-
updated_at: new Date(),
156-
})
157-
.from('users')
158-
.where('oidc_sub', args.oidcSub)
159-
.returning(['*']);
160-
161-
if (!userRecord) {
162-
return null;
147+
try {
148+
const [userRecord]: UserRecord[] = await database
149+
.update({
150+
firstname,
151+
user_title,
152+
lastname,
153+
preferredname,
154+
gender,
155+
birthdate,
156+
institution_id: institutionId,
157+
department,
158+
position,
159+
email,
160+
telephone,
161+
placeholder,
162+
oauth_refresh_token: oauthRefreshToken,
163+
oauth_issuer: oauthIssuer,
164+
updated_at: new Date(),
165+
})
166+
.from('users')
167+
.where('oidc_sub', args.oidcSub)
168+
.returning(['*']);
169+
170+
if (!userRecord) {
171+
return null;
172+
}
173+
174+
return createUserObject(userRecord);
175+
} catch (error) {
176+
if (error instanceof Error && 'code' in error && error.code === '23505') {
177+
throw new GraphQLError('User already exists');
178+
}
179+
throw new GraphQLError('Could not create user. Check your Inputs.');
163180
}
164-
165-
return createUserObject(userRecord);
166181
}
167182

168183
async createInviteUser(args: CreateUserByEmailInviteArgs): Promise<number> {
@@ -363,7 +378,6 @@ export default class PostgresUserDataSource implements UserDataSource {
363378
.then((user: UserRecord) => (!user ? null : createUserObject(user)));
364379
}
365380

366-
// NOTE: This is used in the OAuthAuthorization only where we upsert users returned from Auth server.
367381
async create(
368382
user_title: string | undefined,
369383
firstname: string,
@@ -375,7 +389,7 @@ export default class PostgresUserDataSource implements UserDataSource {
375389
oauth_issuer: string,
376390
gender: string,
377391
birthdate: Date,
378-
institution_id: number,
392+
institution_id: number | undefined,
379393
department: string,
380394
position: string,
381395
email: string,
@@ -407,6 +421,12 @@ export default class PostgresUserDataSource implements UserDataSource {
407421
}
408422

409423
return createUserObject(user[0]);
424+
})
425+
.catch((error) => {
426+
if (error.code === '23505') {
427+
throw new GraphQLError('User already exists');
428+
}
429+
throw new GraphQLError('Could not create user. Check your Inputs.');
410430
});
411431
}
412432

apps/backend/src/datasources/postgres/records.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ export interface UserRecord {
249249
readonly lastname: string;
250250
readonly username: string;
251251
readonly preferredname: string;
252-
readonly oidc_sub: string | null;
252+
readonly oidc_sub: string;
253253
readonly oauth_refresh_token: string | null;
254254
readonly oauth_issuer: string | null;
255255
readonly gender: string;
@@ -996,7 +996,8 @@ export const createBasicUserObject = (
996996
user.placeholder,
997997
user.email,
998998
user.country,
999-
user.user_title
999+
user.user_title,
1000+
user.oidc_sub
10001001
);
10011002
};
10021003

apps/backend/src/datasources/stfc/StfcUserDataSource.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ export function toEssBasicUserDetails(
107107
false,
108108
stfcUser.email ?? '',
109109
stfcUser.country ?? '',
110-
stfcUser.title ?? ''
110+
stfcUser.title ?? '',
111+
''
111112
);
112113
}
113114

apps/backend/src/models/User.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ export class BasicUserDetails {
9191
public placeholder: boolean,
9292
public email: string,
9393
public country: string,
94-
public title: string
94+
public title: string,
95+
public oidc_sub: string
9596
) {}
9697
}
9798

apps/backend/src/mutations/UserMutations.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import {
88
updateUserValidationBackendSchema,
99
} from '@user-office-software/duo-validation';
1010
import * as bcrypt from 'bcryptjs';
11+
import { DateTime } from 'luxon';
1112
import { inject, injectable } from 'tsyringe';
1213
import { Args } from 'type-graphql';
1314

1415
import { UserAuthorization } from '../auth/UserAuthorization';
1516
import { Tokens } from '../config/Tokens';
17+
import { AdminDataSource } from '../datasources/AdminDataSource';
1618
import { RedeemCodesDataSource } from '../datasources/RedeemCodesDataSource';
1719
import { UserDataSource } from '../datasources/UserDataSource';
1820
import { Authorized, EventBus, ValidateArgs } from '../decorators';
@@ -34,6 +36,7 @@ import {
3436
UpdateUserByOidcSubArgs,
3537
UpdateUserByIdArgs,
3638
} from '../resolvers/mutations/UpdateUserMutation';
39+
import { UpsertUserByOidcSubArgs } from '../resolvers/mutations/UpsertUserMutation';
3740
import { signToken, verifyToken } from '../utils/jwt';
3841
import { ApolloServerErrorCodeExtended } from '../utils/utilTypes';
3942

@@ -42,6 +45,7 @@ export default class UserMutations {
4245
constructor(
4346
@inject(Tokens.UserAuthorization) private userAuth: UserAuthorization,
4447
@inject(Tokens.UserDataSource) private dataSource: UserDataSource,
48+
@inject(Tokens.AdminDataSource) private adminDataSource: AdminDataSource,
4549
@inject(Tokens.RedeemCodesDataSource)
4650
private redeemCodeDataSource: RedeemCodesDataSource
4751
) {}
@@ -507,4 +511,84 @@ export default class UserMutations {
507511
): Promise<User | null> {
508512
return this.dataSource.setUserNotPlaceholder(id);
509513
}
514+
515+
@Authorized([Roles.USER_OFFICER])
516+
async upsertUserByOidcSub(
517+
agent: UserWithRole | null,
518+
args: UpsertUserByOidcSubArgs
519+
) {
520+
const {
521+
userTitle,
522+
firstName,
523+
lastName,
524+
username,
525+
preferredName,
526+
oidcSub,
527+
gender,
528+
birthDate,
529+
institutionRoRId,
530+
department,
531+
position,
532+
email,
533+
telephone,
534+
} = args;
535+
536+
const userWithOAuthSubMatch = await this.dataSource.getByOIDCSub(oidcSub);
537+
538+
let formattedBirthDate: DateTime | null = null;
539+
formattedBirthDate = birthDate ? DateTime.fromISO(birthDate) : null;
540+
if (formattedBirthDate && !formattedBirthDate.isValid) {
541+
return rejection('Invalid birth date format', { birthDate, args });
542+
}
543+
544+
// Fetch Institution ID from ROR ID
545+
const institution =
546+
await this.adminDataSource.getInstitutionByRorId(institutionRoRId);
547+
548+
if (userWithOAuthSubMatch) {
549+
const updatedUser = await this.dataSource.update({
550+
...userWithOAuthSubMatch,
551+
birthdate: formattedBirthDate?.toJSDate(),
552+
department: department ?? undefined,
553+
email,
554+
firstname: firstName,
555+
username: username ?? undefined,
556+
gender: gender ?? undefined,
557+
lastname: lastName,
558+
oidcSub: oidcSub,
559+
institutionId: institution?.id,
560+
position: position,
561+
preferredname: preferredName ?? undefined,
562+
telephone: telephone ?? undefined,
563+
user_title: userTitle ?? undefined,
564+
});
565+
566+
return updatedUser;
567+
} else {
568+
const newUser = await this.dataSource.create(
569+
userTitle ?? '',
570+
firstName,
571+
lastName,
572+
username ?? '',
573+
preferredName ?? '',
574+
oidcSub,
575+
'',
576+
'',
577+
gender ?? '',
578+
formattedBirthDate?.toJSDate() ?? new Date(),
579+
institution?.id ?? undefined,
580+
department ?? '',
581+
position,
582+
email,
583+
telephone ?? ''
584+
);
585+
586+
await this.dataSource.addUserRole({
587+
userID: newUser.id,
588+
roleID: UserRole.USER,
589+
});
590+
591+
return newUser;
592+
}
593+
}
510594
}

apps/backend/src/queries/UserQueries.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ export default class UserQueries {
7373
user.placeholder,
7474
user.email,
7575
user.country,
76-
user.title
76+
user.title,
77+
user.oidc_sub
7778
);
7879
} else {
7980
return null;
@@ -107,7 +108,8 @@ export default class UserQueries {
107108
user.placeholder,
108109
user.email,
109110
user.country,
110-
user.title
111+
user.title,
112+
user.oidc_sub
111113
);
112114
}
113115

0 commit comments

Comments
 (0)