chore: rename linkToken to oauthLinkToken

This commit is contained in:
bo0tzz
2026-04-17 02:22:15 +02:00
parent b3e5ec48e6
commit b8c373f0f1
12 changed files with 63 additions and 27 deletions

View File

@@ -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();
});
});
});

View File

@@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String>(json, r'email')!,
oauthLinkToken: mapValueOfType<String>(json, r'oauthLinkToken'),
password: mapValueOfType<String>(json, r'password')!,
);
}

View File

@@ -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<String, dynamic> toJson() {
final json = <String, dynamic>{};
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<String>(json, r'email')!,
name: mapValueOfType<String>(json, r'name')!,
oauthLinkToken: mapValueOfType<String>(json, r'oauthLinkToken'),
password: mapValueOfType<String>(json, r'password')!,
);
}

View File

@@ -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",

View File

@@ -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;
};

View File

@@ -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' });

View File

@@ -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');
});
});

View File

@@ -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,
});
}
}

View File

@@ -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',

View File

@@ -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'));

View File

@@ -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;

View File

@@ -72,7 +72,7 @@
} catch (error) {
if (isHttpError(error) && error.data?.message === 'oauth_account_link_required') {
const errorData = error.data as unknown as Record<string, string>;
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);