From 24929cb14fdb314a6d6283bee6ff72267107e7aa Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Mon, 12 Jan 2026 16:06:35 +0100 Subject: [PATCH 1/2] feat: add unique id to users to be able to match users with external registration services --- apps/backend/db_patches/0203_AlterUsersUniqueId.sql | 11 +++++++++++ apps/backend/src/auth/OAuthAuthorization.ts | 5 +++-- apps/backend/src/datasources/UserDataSource.ts | 5 +++-- .../datasources/mockups/DataAccessUsersDataSource.ts | 2 ++ .../backend/src/datasources/mockups/UserDataSource.ts | 8 +++++++- .../src/datasources/postgres/UserDataSource.ts | 4 +++- apps/backend/src/datasources/postgres/records.ts | 4 +++- .../src/datasources/stfc/StfcUserDataSource.ts | 1 + apps/backend/src/models/User.ts | 1 + apps/backend/src/mutations/UserMutations.ts | 7 ++++--- .../src/resolvers/mutations/UpdateUserMutation.ts | 2 +- 11 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 apps/backend/db_patches/0203_AlterUsersUniqueId.sql diff --git a/apps/backend/db_patches/0203_AlterUsersUniqueId.sql b/apps/backend/db_patches/0203_AlterUsersUniqueId.sql new file mode 100644 index 0000000000..2104b4012a --- /dev/null +++ b/apps/backend/db_patches/0203_AlterUsersUniqueId.sql @@ -0,0 +1,11 @@ +DO +$$ +BEGIN + IF register_patch('AlterUsersUniqueId.sql', 'Gergely Nyiri', 'Adding column unique_id to users', '2026-01-12') THEN + BEGIN + ALTER TABLE users ADD COLUMN unique_id varchar(100) UNIQUE; + END; + END IF; +END; +$$ +LANGUAGE plpgsql; diff --git a/apps/backend/src/auth/OAuthAuthorization.ts b/apps/backend/src/auth/OAuthAuthorization.ts index e643217b54..a83e5daa5e 100644 --- a/apps/backend/src/auth/OAuthAuthorization.ts +++ b/apps/backend/src/auth/OAuthAuthorization.ts @@ -1,5 +1,5 @@ -import 'reflect-metadata'; import { env } from 'process'; +import 'reflect-metadata'; import { logger } from '@user-office-software/duo-logger'; import { OpenIdClient } from '@user-office-software/openid'; @@ -176,7 +176,8 @@ export class OAuthAuthorization extends UserAuthorization { '', (userInfo.position as string) ?? '', userInfo.email, - '' + '', + userInfo.unique_id as string ); const roleID = this.getUserRole(newUser); diff --git a/apps/backend/src/datasources/UserDataSource.ts b/apps/backend/src/datasources/UserDataSource.ts index 29c9447fa7..e93a8b175a 100644 --- a/apps/backend/src/datasources/UserDataSource.ts +++ b/apps/backend/src/datasources/UserDataSource.ts @@ -5,8 +5,8 @@ import { BasicUserDetails, User, UserRole } from '../models/User'; import { AddUserRoleArgs } from '../resolvers/mutations/AddUserRoleMutation'; import { CreateUserByEmailInviteArgs } from '../resolvers/mutations/CreateUserByEmailInviteMutation'; import { - UpdateUserByOidcSubArgs, UpdateUserByIdArgs, + UpdateUserByOidcSubArgs, } from '../resolvers/mutations/UpdateUserMutation'; import { UsersArgs } from '../resolvers/queries/UsersQuery'; @@ -72,7 +72,8 @@ export interface UserDataSource { department: string, position: string, email: string, - telephone: string + telephone: string, + unique_id: string | undefined ): Promise; ensureDummyUserExists(userId: number): Promise; ensureDummyUsersExist(userIds: number[]): Promise; diff --git a/apps/backend/src/datasources/mockups/DataAccessUsersDataSource.ts b/apps/backend/src/datasources/mockups/DataAccessUsersDataSource.ts index 2f0b066253..0b16cce587 100644 --- a/apps/backend/src/datasources/mockups/DataAccessUsersDataSource.ts +++ b/apps/backend/src/datasources/mockups/DataAccessUsersDataSource.ts @@ -17,6 +17,7 @@ export const dummyDataAccessFullUser = new User( 'jane.smith', 'Jane', 'jane.smith.oidc', + 'jane.smith.uniqueid', 'refresh-token', 'oauth-issuer', 'female', @@ -40,6 +41,7 @@ export const dummyDataAccessFullUser2 = new User( 'bob.johnson', 'Bob', 'bob.johnson.oidc', + 'bob.johnson.uniqueid', 'refresh-token', 'oauth-issuer', 'male', diff --git a/apps/backend/src/datasources/mockups/UserDataSource.ts b/apps/backend/src/datasources/mockups/UserDataSource.ts index 7e49445120..7300e95085 100644 --- a/apps/backend/src/datasources/mockups/UserDataSource.ts +++ b/apps/backend/src/datasources/mockups/UserDataSource.ts @@ -56,6 +56,7 @@ export const dummyUserOfficer = new User( 'JoDo', 'Hailey', '324235', + '182082741', '683142616', 'issuer', 'male', @@ -90,6 +91,7 @@ export const dummyUser = new User( 'JaDa', 'Meta', '12312414', + '182082741', '568567353', 'issuer', 'male', @@ -209,6 +211,7 @@ export const dummyPlaceHolderUser = new User( 'JaDa', 'Meta', '12312414', + '182082741', '568567353', 'issuer', 'male', @@ -232,6 +235,7 @@ export const dummyUserNotOnProposal = new User( 'NoDO', 'Damion', '182082741', + '182082741', 'Apricot', 'issuer', 'female', @@ -514,7 +518,8 @@ export class UserDataSourceMock implements UserDataSource { department: string, position: string, email: string, - telephone: string + telephone: string, + unique_id: string | undefined ) { // Generate a new user ID const newId = Math.max(...this.mockUsers.map((u) => u.id)) + 1; @@ -527,6 +532,7 @@ export class UserDataSourceMock implements UserDataSource { username, preferredname || '', oidc_sub, + unique_id, oauth_refresh_token, oauth_issuer, gender || 'unspecified', diff --git a/apps/backend/src/datasources/postgres/UserDataSource.ts b/apps/backend/src/datasources/postgres/UserDataSource.ts index 57bab767d8..91bde020b9 100644 --- a/apps/backend/src/datasources/postgres/UserDataSource.ts +++ b/apps/backend/src/datasources/postgres/UserDataSource.ts @@ -395,7 +395,8 @@ export default class PostgresUserDataSource implements UserDataSource { department: string, position: string, email: string, - telephone: string + telephone: string, + unique_id: string | undefined ): Promise { return database .insert({ @@ -414,6 +415,7 @@ export default class PostgresUserDataSource implements UserDataSource { position, email, telephone, + unique_id, }) .returning(['*']) .into('users') diff --git a/apps/backend/src/datasources/postgres/records.ts b/apps/backend/src/datasources/postgres/records.ts index d3abb964b8..53839ab3e2 100644 --- a/apps/backend/src/datasources/postgres/records.ts +++ b/apps/backend/src/datasources/postgres/records.ts @@ -1,6 +1,6 @@ import { - ProposalPdfTemplateRecord, ExperimentSafetyPdfTemplateRecord, + ProposalPdfTemplateRecord, } from 'knex/types/tables'; import { EmailTemplateId } from '../../eventHandlers/email/essEmailHandler'; @@ -251,6 +251,7 @@ export interface UserRecord { readonly username: string; readonly preferredname: string; readonly oidc_sub: string | null; + readonly unique_id: string | undefined; readonly oauth_refresh_token: string | null; readonly oauth_issuer: string | null; readonly gender: string; @@ -971,6 +972,7 @@ export const createUserObject = (user: UserRecord) => { user.username, user.preferredname, user.oidc_sub, + user.unique_id, user.oauth_refresh_token, user.oauth_issuer, user.gender, diff --git a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts index 300185b29a..e197e670fb 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts @@ -121,6 +121,7 @@ function toEssUser(stfcUser: StfcBasicPersonDetails): User { stfcUser.email ?? '', stfcUser.firstNameKnownAs ?? stfcUser.givenName, '', + undefined, '', '', '', diff --git a/apps/backend/src/models/User.ts b/apps/backend/src/models/User.ts index 42b8382fc3..6a29a081b2 100644 --- a/apps/backend/src/models/User.ts +++ b/apps/backend/src/models/User.ts @@ -30,6 +30,7 @@ export class User { public username: string, public preferredname: string | undefined, public oidcSub: string | null, + public uniqueId: string | undefined, public oauthRefreshToken: string | null, public oauthIssuer: string | null, public gender: string, diff --git a/apps/backend/src/mutations/UserMutations.ts b/apps/backend/src/mutations/UserMutations.ts index 6a7a0604d1..eb2e356dbc 100644 --- a/apps/backend/src/mutations/UserMutations.ts +++ b/apps/backend/src/mutations/UserMutations.ts @@ -32,9 +32,9 @@ import { import { AddUserRoleArgs } from '../resolvers/mutations/AddUserRoleMutation'; import { CreateUserByEmailInviteArgs } from '../resolvers/mutations/CreateUserByEmailInviteMutation'; import { - UpdateUserRolesArgs, - UpdateUserByOidcSubArgs, UpdateUserByIdArgs, + UpdateUserByOidcSubArgs, + UpdateUserRolesArgs, } from '../resolvers/mutations/UpdateUserMutation'; import { UpsertUserByOidcSubArgs } from '../resolvers/mutations/UpsertUserMutation'; import { signToken, verifyToken } from '../utils/jwt'; @@ -591,7 +591,8 @@ export default class UserMutations { department ?? '', position, email, - telephone ?? '' + telephone ?? '', + undefined ); await this.dataSource.addUserRole({ diff --git a/apps/backend/src/resolvers/mutations/UpdateUserMutation.ts b/apps/backend/src/resolvers/mutations/UpdateUserMutation.ts index 3091fc96a5..5e817da1b0 100644 --- a/apps/backend/src/resolvers/mutations/UpdateUserMutation.ts +++ b/apps/backend/src/resolvers/mutations/UpdateUserMutation.ts @@ -1,4 +1,5 @@ import { + Arg, Args, ArgsType, Ctx, @@ -6,7 +7,6 @@ import { Int, Mutation, Resolver, - Arg, } from 'type-graphql'; import { ResolverContext } from '../../context'; From 9323fda491e9a888b6adccaabd2dedeae1268deb Mon Sep 17 00:00:00 2001 From: Gergely Nyiri Date: Mon, 12 Jan 2026 16:38:15 +0100 Subject: [PATCH 2/2] fix: undefined vs null mismatch --- apps/backend/src/datasources/UserDataSource.ts | 2 +- apps/backend/src/datasources/mockups/UserDataSource.ts | 2 +- apps/backend/src/datasources/postgres/UserDataSource.ts | 2 +- apps/backend/src/datasources/postgres/records.ts | 2 +- apps/backend/src/datasources/stfc/StfcUserDataSource.ts | 2 +- apps/backend/src/models/User.ts | 2 +- apps/backend/src/mutations/UserMutations.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/backend/src/datasources/UserDataSource.ts b/apps/backend/src/datasources/UserDataSource.ts index e93a8b175a..69411823bb 100644 --- a/apps/backend/src/datasources/UserDataSource.ts +++ b/apps/backend/src/datasources/UserDataSource.ts @@ -73,7 +73,7 @@ export interface UserDataSource { position: string, email: string, telephone: string, - unique_id: string | undefined + unique_id: string | null ): Promise; ensureDummyUserExists(userId: number): Promise; ensureDummyUsersExist(userIds: number[]): Promise; diff --git a/apps/backend/src/datasources/mockups/UserDataSource.ts b/apps/backend/src/datasources/mockups/UserDataSource.ts index 7300e95085..46d24b3c62 100644 --- a/apps/backend/src/datasources/mockups/UserDataSource.ts +++ b/apps/backend/src/datasources/mockups/UserDataSource.ts @@ -519,7 +519,7 @@ export class UserDataSourceMock implements UserDataSource { position: string, email: string, telephone: string, - unique_id: string | undefined + unique_id: string | null ) { // Generate a new user ID const newId = Math.max(...this.mockUsers.map((u) => u.id)) + 1; diff --git a/apps/backend/src/datasources/postgres/UserDataSource.ts b/apps/backend/src/datasources/postgres/UserDataSource.ts index 91bde020b9..fff01c6890 100644 --- a/apps/backend/src/datasources/postgres/UserDataSource.ts +++ b/apps/backend/src/datasources/postgres/UserDataSource.ts @@ -396,7 +396,7 @@ export default class PostgresUserDataSource implements UserDataSource { position: string, email: string, telephone: string, - unique_id: string | undefined + unique_id: string | null ): Promise { return database .insert({ diff --git a/apps/backend/src/datasources/postgres/records.ts b/apps/backend/src/datasources/postgres/records.ts index 53839ab3e2..a970a8587c 100644 --- a/apps/backend/src/datasources/postgres/records.ts +++ b/apps/backend/src/datasources/postgres/records.ts @@ -251,7 +251,7 @@ export interface UserRecord { readonly username: string; readonly preferredname: string; readonly oidc_sub: string | null; - readonly unique_id: string | undefined; + readonly unique_id: string | null; readonly oauth_refresh_token: string | null; readonly oauth_issuer: string | null; readonly gender: string; diff --git a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts index e197e670fb..bf6268611d 100644 --- a/apps/backend/src/datasources/stfc/StfcUserDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcUserDataSource.ts @@ -121,7 +121,7 @@ function toEssUser(stfcUser: StfcBasicPersonDetails): User { stfcUser.email ?? '', stfcUser.firstNameKnownAs ?? stfcUser.givenName, '', - undefined, + null, '', '', '', diff --git a/apps/backend/src/models/User.ts b/apps/backend/src/models/User.ts index 6a29a081b2..abdbf27c74 100644 --- a/apps/backend/src/models/User.ts +++ b/apps/backend/src/models/User.ts @@ -30,7 +30,7 @@ export class User { public username: string, public preferredname: string | undefined, public oidcSub: string | null, - public uniqueId: string | undefined, + public uniqueId: string | null, public oauthRefreshToken: string | null, public oauthIssuer: string | null, public gender: string, diff --git a/apps/backend/src/mutations/UserMutations.ts b/apps/backend/src/mutations/UserMutations.ts index eb2e356dbc..a3b2b10d0a 100644 --- a/apps/backend/src/mutations/UserMutations.ts +++ b/apps/backend/src/mutations/UserMutations.ts @@ -592,7 +592,7 @@ export default class UserMutations { position, email, telephone ?? '', - undefined + null ); await this.dataSource.addUserRole({