From b8c373f0f1775716d6ff784cd7b91ad6fb16b750 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Fri, 17 Apr 2026 02:22:15 +0200 Subject: [PATCH] chore: rename linkToken to oauthLinkToken --- e2e/src/specs/server/api/oauth.e2e-spec.ts | 2 +- .../lib/model/login_credential_dto.dart | 20 ++++++++++++++++++- mobile/openapi/lib/model/sign_up_dto.dart | 20 ++++++++++++++++++- open-api/immich-openapi-specs.json | 10 +++++----- open-api/typescript-sdk/src/fetch-client.ts | 6 +++--- server/src/dtos/auth.dto.ts | 2 +- server/src/services/auth.service.spec.ts | 12 +++++------ server/src/services/auth.service.ts | 6 +++--- web/src/lib/route.ts | 2 +- web/src/routes/auth/link/+page.svelte | 4 ++-- web/src/routes/auth/link/+page.ts | 4 ++-- web/src/routes/auth/login/+page.svelte | 2 +- 12 files changed, 63 insertions(+), 27 deletions(-) diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index 6ff2446fe8..d7edbb5a6e 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -367,7 +367,7 @@ describe(`/oauth`, () => { expect(status).toBe(403); expect(body.message).toBe('oauth_account_link_required'); expect(body.userEmail).toBe('oauth-user3@immich.app'); - expect(body.linkToken).toBeDefined(); + expect(body.oauthLinkToken).toBeDefined(); }); }); }); diff --git a/mobile/openapi/lib/model/login_credential_dto.dart b/mobile/openapi/lib/model/login_credential_dto.dart index 1fdfdc3d40..693bd9b5ed 100644 --- a/mobile/openapi/lib/model/login_credential_dto.dart +++ b/mobile/openapi/lib/model/login_credential_dto.dart @@ -14,32 +14,49 @@ class LoginCredentialDto { /// Returns a new [LoginCredentialDto] instance. LoginCredentialDto({ required this.email, + this.oauthLinkToken, required this.password, }); /// User email String email; + /// OAuth link token to consume on successful login + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? oauthLinkToken; + /// User password String password; @override bool operator ==(Object other) => identical(this, other) || other is LoginCredentialDto && other.email == email && + other.oauthLinkToken == oauthLinkToken && other.password == password; @override int get hashCode => // ignore: unnecessary_parenthesis (email.hashCode) + + (oauthLinkToken == null ? 0 : oauthLinkToken!.hashCode) + (password.hashCode); @override - String toString() => 'LoginCredentialDto[email=$email, password=$password]'; + String toString() => 'LoginCredentialDto[email=$email, oauthLinkToken=$oauthLinkToken, password=$password]'; Map toJson() { final json = {}; json[r'email'] = this.email; + if (this.oauthLinkToken != null) { + json[r'oauthLinkToken'] = this.oauthLinkToken; + } else { + // json[r'oauthLinkToken'] = null; + } json[r'password'] = this.password; return json; } @@ -54,6 +71,7 @@ class LoginCredentialDto { return LoginCredentialDto( email: mapValueOfType(json, r'email')!, + oauthLinkToken: mapValueOfType(json, r'oauthLinkToken'), password: mapValueOfType(json, r'password')!, ); } diff --git a/mobile/openapi/lib/model/sign_up_dto.dart b/mobile/openapi/lib/model/sign_up_dto.dart index 54c8fa07d2..a6c60c60a8 100644 --- a/mobile/openapi/lib/model/sign_up_dto.dart +++ b/mobile/openapi/lib/model/sign_up_dto.dart @@ -15,6 +15,7 @@ class SignUpDto { SignUpDto({ required this.email, required this.name, + this.oauthLinkToken, required this.password, }); @@ -24,6 +25,15 @@ class SignUpDto { /// User name String name; + /// OAuth link token to consume on successful login + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? oauthLinkToken; + /// User password String password; @@ -31,6 +41,7 @@ class SignUpDto { bool operator ==(Object other) => identical(this, other) || other is SignUpDto && other.email == email && other.name == name && + other.oauthLinkToken == oauthLinkToken && other.password == password; @override @@ -38,15 +49,21 @@ class SignUpDto { // ignore: unnecessary_parenthesis (email.hashCode) + (name.hashCode) + + (oauthLinkToken == null ? 0 : oauthLinkToken!.hashCode) + (password.hashCode); @override - String toString() => 'SignUpDto[email=$email, name=$name, password=$password]'; + String toString() => 'SignUpDto[email=$email, name=$name, oauthLinkToken=$oauthLinkToken, password=$password]'; Map toJson() { final json = {}; json[r'email'] = this.email; json[r'name'] = this.name; + if (this.oauthLinkToken != null) { + json[r'oauthLinkToken'] = this.oauthLinkToken; + } else { + // json[r'oauthLinkToken'] = null; + } json[r'password'] = this.password; return json; } @@ -62,6 +79,7 @@ class SignUpDto { return SignUpDto( email: mapValueOfType(json, r'email')!, name: mapValueOfType(json, r'name')!, + oauthLinkToken: mapValueOfType(json, r'oauthLinkToken'), password: mapValueOfType(json, r'password')!, ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b743441bfd..89f9d0d932 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -18013,7 +18013,7 @@ "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, - "linkToken": { + "oauthLinkToken": { "description": "OAuth link token to consume on successful login", "type": "string" }, @@ -21799,15 +21799,15 @@ "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, - "linkToken": { - "description": "OAuth link token to consume on successful login", - "type": "string" - }, "name": { "description": "User name", "example": "Admin", "type": "string" }, + "oauthLinkToken": { + "description": "OAuth link token to consume on successful login", + "type": "string" + }, "password": { "description": "User password", "example": "password", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 29829ca408..219e947845 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1008,10 +1008,10 @@ export type AssetOcrResponseDto = { export type SignUpDto = { /** User email */ email: string; - /** OAuth link token to consume on successful login */ - linkToken?: string; /** User name */ name: string; + /** OAuth link token to consume on successful login */ + oauthLinkToken?: string; /** User password */ password: string; }; @@ -1027,7 +1027,7 @@ export type LoginCredentialDto = { /** User email */ email: string; /** OAuth link token to consume on successful login */ - linkToken?: string; + oauthLinkToken?: string; /** User password */ password: string; }; diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index b111810381..54e7fae0bc 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -23,7 +23,7 @@ const LoginCredentialSchema = z .object({ email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }), password: z.string().describe('User password').meta({ example: 'password' }), - linkToken: z.string().optional().describe('OAuth link token to consume on successful login'), + oauthLinkToken: z.string().optional().describe('OAuth link token to consume on successful login'), }) .meta({ id: 'LoginCredentialDto' }); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 18b90151d5..64e99294b1 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -89,7 +89,7 @@ describe(AuthService.name, () => { expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); }); - it('should link an OAuth account when linkToken is provided', async () => { + it('should link an OAuth account when oauthLinkToken is provided', async () => { const user = UserFactory.create({ password: 'immich_password' }); const session = SessionFactory.create(); mocks.user.getByEmail.mockResolvedValue(user); @@ -104,20 +104,20 @@ describe(AuthService.name, () => { }); mocks.user.update.mockResolvedValue(user); - await sut.login({ email, password: 'password', linkToken: 'plain-token' }, loginDetails); + await sut.login({ email, password: 'password', oauthLinkToken: 'plain-token' }, loginDetails); expect(mocks.oauthLinkToken.consumeToken).toHaveBeenCalledTimes(1); expect(mocks.user.update).toHaveBeenCalledWith(user.id, { oauthId: 'oauth-sub-123' }); }); - it('should reject login with invalid linkToken', async () => { + it('should reject login with invalid oauthLinkToken', async () => { const user = UserFactory.create({ password: 'immich_password' }); mocks.user.getByEmail.mockResolvedValue(user); mocks.oauthLinkToken.consumeToken.mockResolvedValue(null as any); - await expect(sut.login({ email, password: 'password', linkToken: 'bad-token' }, loginDetails)).rejects.toThrow( - 'Invalid or expired link token', - ); + await expect( + sut.login({ email, password: 'password', oauthLinkToken: 'bad-token' }, loginDetails), + ).rejects.toThrow('Invalid or expired link token'); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 03f6e3ead8..9d21e2c225 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -75,8 +75,8 @@ export class AuthService extends BaseService { throw new UnauthorizedException('Incorrect email or password'); } - if (dto.linkToken) { - const hashedToken = this.cryptoRepository.hashSha256(dto.linkToken); + if (dto.oauthLinkToken) { + const hashedToken = this.cryptoRepository.hashSha256(dto.oauthLinkToken); const record = await this.oauthLinkTokenRepository.consumeToken(hashedToken); if (!record) { throw new BadRequestException('Invalid or expired link token'); @@ -350,7 +350,7 @@ export class AuthService extends BaseService { throw new ForbiddenException({ message: 'oauth_account_link_required', userEmail: emailUser.email, - linkToken: plainToken, + oauthLinkToken: plainToken, }); } } diff --git a/web/src/lib/route.ts b/web/src/lib/route.ts index 4afa75e188..f3ba7aa67c 100644 --- a/web/src/lib/route.ts +++ b/web/src/lib/route.ts @@ -51,7 +51,7 @@ export const Docs = { export const Route = { // auth login: (params?: { continue?: string; autoLaunch?: 0 | 1 }) => '/auth/login' + asQueryString(params), - authLink: (params?: { linkToken?: string; email?: string }) => '/auth/link' + asQueryString(params), + authLink: (params?: { oauthLinkToken?: string; email?: string }) => '/auth/link' + asQueryString(params), logout: (params?: { continue?: string }) => '/auth/logout' + asQueryString(params), register: () => '/auth/register', changePassword: () => '/auth/change-password', diff --git a/web/src/routes/auth/link/+page.svelte b/web/src/routes/auth/link/+page.svelte index 83af8021c8..65922e631d 100644 --- a/web/src/routes/auth/link/+page.svelte +++ b/web/src/routes/auth/link/+page.svelte @@ -18,7 +18,7 @@ let { data }: Props = $props(); - let linkToken = $state(data.linkToken); + let oauthLinkToken = $state(data.oauthLinkToken); let email = $state(data.email || authManager.user?.email || ''); let password = $state(''); let errorMessage = $state(''); @@ -33,7 +33,7 @@ try { errorMessage = ''; loading = true; - const user = await login({ loginCredentialDto: { email, password, linkToken } }); + const user = await login({ loginCredentialDto: { email, password, oauthLinkToken } }); eventManager.emit('AuthLogin', user); await authManager.refresh(); toastManager.primary($t('linked_oauth_account')); diff --git a/web/src/routes/auth/link/+page.ts b/web/src/routes/auth/link/+page.ts index d37d0d7cc7..11d37855b4 100644 --- a/web/src/routes/auth/link/+page.ts +++ b/web/src/routes/auth/link/+page.ts @@ -2,7 +2,7 @@ import { getFormatter } from '$lib/utils/i18n'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { - const linkToken = url.searchParams.get('linkToken') || ''; + const oauthLinkToken = url.searchParams.get('oauthLinkToken') || ''; const email = url.searchParams.get('email') || ''; const $t = await getFormatter(); @@ -10,7 +10,7 @@ export const load = (async ({ url }) => { meta: { title: $t('link_to_oauth'), }, - linkToken, + oauthLinkToken, email, }; }) satisfies PageLoad; diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 952657a35b..5353af1683 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -72,7 +72,7 @@ } catch (error) { if (isHttpError(error) && error.data?.message === 'oauth_account_link_required') { const errorData = error.data as unknown as Record; - await goto(Route.authLink({ linkToken: errorData.linkToken, email: errorData.userEmail })); + await goto(Route.authLink({ oauthLinkToken: errorData.oauthLinkToken, email: errorData.userEmail })); return; } console.error('Error [login-form] [oauth.callback]', error);