mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
fix: review notes
This commit is contained in:
@@ -364,9 +364,10 @@ describe(`/oauth`, () => {
|
|||||||
});
|
});
|
||||||
const callbackParams = await loginWithOAuth('oauth-user3');
|
const callbackParams = await loginWithOAuth('oauth-user3');
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(403);
|
||||||
expect(body.message).toBe('oauth_account_link_required');
|
expect(body.message).toBe('oauth_account_link_required');
|
||||||
expect(body.userEmail).toBe('oauth-user3@immich.app');
|
expect(body.userEmail).toBe('oauth-user3@immich.app');
|
||||||
|
expect(body.linkToken).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -1642,8 +1642,8 @@
|
|||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"notifications_setting_description": "Manage notifications",
|
"notifications_setting_description": "Manage notifications",
|
||||||
"oauth": "OAuth",
|
"oauth": "OAuth",
|
||||||
"oauth_link_existing_account": "Log in with your Immich password to link your OAuth account.",
|
"oauth_link_existing_account": "Log in with your Immich password to link your OAuth account",
|
||||||
"oauth_link_password_login_required": "An account with this email already exists. Password login is required to link your OAuth account. Please contact your administrator.",
|
"oauth_link_password_login_required": "An account with this email already exists but password login is required to link your OAuth account. Please contact your administrator",
|
||||||
"obtainium_configurator": "Obtainium Configurator",
|
"obtainium_configurator": "Obtainium Configurator",
|
||||||
"obtainium_configurator_instructions": "Use Obtainium to install and update the Android app directly from Immich GitHub's release. Create an API key and select a variant to create your Obtainium configuration link",
|
"obtainium_configurator_instructions": "Use Obtainium to install and update the Android app directly from Immich GitHub's release. Create an API key and select a variant to create your Obtainium configuration link",
|
||||||
"ocr": "OCR",
|
"ocr": "OCR",
|
||||||
|
|||||||
@@ -7439,65 +7439,6 @@
|
|||||||
"x-immich-state": "Stable"
|
"x-immich-state": "Stable"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/oauth/link": {
|
|
||||||
"post": {
|
|
||||||
"description": "Link an OAuth account to the authenticated user.",
|
|
||||||
"operationId": "linkOAuthAccount",
|
|
||||||
"parameters": [],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/OAuthLinkDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UserAdminResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "Link OAuth account",
|
|
||||||
"tags": [
|
|
||||||
"Authentication"
|
|
||||||
],
|
|
||||||
"x-immich-history": [
|
|
||||||
{
|
|
||||||
"version": "v1",
|
|
||||||
"state": "Added"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "v1",
|
|
||||||
"state": "Beta"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "v2",
|
|
||||||
"state": "Stable"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"x-immich-state": "Stable"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/oauth/mobile-redirect": {
|
"/oauth/mobile-redirect": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting.",
|
"description": "Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting.",
|
||||||
@@ -18072,6 +18013,10 @@
|
|||||||
"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])?)*$",
|
"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"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"linkToken": {
|
||||||
|
"description": "OAuth link token to consume on successful login",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"description": "User password",
|
"description": "User password",
|
||||||
"example": "password",
|
"example": "password",
|
||||||
@@ -19116,27 +19061,6 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"OAuthLinkDto": {
|
|
||||||
"properties": {
|
|
||||||
"codeVerifier": {
|
|
||||||
"description": "OAuth code verifier (PKCE)",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"linkToken": {
|
|
||||||
"description": "OAuth link token from prior callback",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"description": "OAuth state parameter",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"description": "OAuth callback URL",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"OAuthTokenEndpointAuthMethod": {
|
"OAuthTokenEndpointAuthMethod": {
|
||||||
"description": "OAuth token endpoint auth method",
|
"description": "OAuth token endpoint auth method",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -21875,6 +21799,10 @@
|
|||||||
"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])?)*$",
|
"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"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"linkToken": {
|
||||||
|
"description": "OAuth link token to consume on successful login",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"description": "User name",
|
"description": "User name",
|
||||||
"example": "Admin",
|
"example": "Admin",
|
||||||
|
|||||||
@@ -1008,6 +1008,8 @@ export type AssetOcrResponseDto = {
|
|||||||
export type SignUpDto = {
|
export type SignUpDto = {
|
||||||
/** User email */
|
/** User email */
|
||||||
email: string;
|
email: string;
|
||||||
|
/** OAuth link token to consume on successful login */
|
||||||
|
linkToken?: string;
|
||||||
/** User name */
|
/** User name */
|
||||||
name: string;
|
name: string;
|
||||||
/** User password */
|
/** User password */
|
||||||
@@ -1024,6 +1026,8 @@ export type ChangePasswordDto = {
|
|||||||
export type LoginCredentialDto = {
|
export type LoginCredentialDto = {
|
||||||
/** User email */
|
/** User email */
|
||||||
email: string;
|
email: string;
|
||||||
|
/** OAuth link token to consume on successful login */
|
||||||
|
linkToken?: string;
|
||||||
/** User password */
|
/** User password */
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
@@ -1421,16 +1425,6 @@ export type OAuthCallbackDto = {
|
|||||||
/** OAuth callback URL */
|
/** OAuth callback URL */
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
export type OAuthLinkDto = {
|
|
||||||
/** OAuth code verifier (PKCE) */
|
|
||||||
codeVerifier?: string;
|
|
||||||
/** OAuth link token from prior callback */
|
|
||||||
linkToken?: string;
|
|
||||||
/** OAuth state parameter */
|
|
||||||
state?: string;
|
|
||||||
/** OAuth callback URL */
|
|
||||||
url?: string;
|
|
||||||
};
|
|
||||||
export type PartnerResponseDto = {
|
export type PartnerResponseDto = {
|
||||||
avatarColor: UserAvatarColor;
|
avatarColor: UserAvatarColor;
|
||||||
/** User email */
|
/** User email */
|
||||||
@@ -4954,21 +4948,6 @@ export function finishOAuth({ oAuthCallbackDto }: {
|
|||||||
body: oAuthCallbackDto
|
body: oAuthCallbackDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Link OAuth account
|
|
||||||
*/
|
|
||||||
export function linkOAuthAccount({ oAuthLinkDto }: {
|
|
||||||
oAuthLinkDto: OAuthLinkDto;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: UserAdminResponseDto;
|
|
||||||
}>("/oauth/link", oazapfts.json({
|
|
||||||
...opts,
|
|
||||||
method: "POST",
|
|
||||||
body: oAuthLinkDto
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Redirect OAuth to mobile
|
* Redirect OAuth to mobile
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
OAuthBackchannelLogoutDto,
|
OAuthBackchannelLogoutDto,
|
||||||
OAuthCallbackDto,
|
OAuthCallbackDto,
|
||||||
OAuthConfigDto,
|
OAuthConfigDto,
|
||||||
OAuthLinkDto,
|
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||||
import { ApiTag, AuthType, ImmichCookie } from 'src/enum';
|
import { ApiTag, AuthType, ImmichCookie } from 'src/enum';
|
||||||
@@ -87,22 +86,6 @@ export class OAuthController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('link')
|
|
||||||
@Authenticated()
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Endpoint({
|
|
||||||
summary: 'Link OAuth account',
|
|
||||||
description: 'Link an OAuth account to the authenticated user.',
|
|
||||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
|
||||||
})
|
|
||||||
linkOAuthAccount(
|
|
||||||
@Req() request: Request,
|
|
||||||
@Auth() auth: AuthDto,
|
|
||||||
@Body() dto: OAuthLinkDto,
|
|
||||||
): Promise<UserAdminResponseDto> {
|
|
||||||
return this.service.link(auth, dto, request.headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('unlink')
|
@Post('unlink')
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const LoginCredentialSchema = z
|
|||||||
.object({
|
.object({
|
||||||
email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }),
|
email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }),
|
||||||
password: z.string().describe('User password').meta({ example: 'password' }),
|
password: z.string().describe('User password').meta({ example: 'password' }),
|
||||||
|
linkToken: z.string().optional().describe('OAuth link token to consume on successful login'),
|
||||||
})
|
})
|
||||||
.meta({ id: 'LoginCredentialDto' });
|
.meta({ id: 'LoginCredentialDto' });
|
||||||
|
|
||||||
@@ -110,18 +111,6 @@ const OAuthCallbackSchema = z
|
|||||||
})
|
})
|
||||||
.meta({ id: 'OAuthCallbackDto' });
|
.meta({ id: 'OAuthCallbackDto' });
|
||||||
|
|
||||||
const OAuthLinkSchema = z
|
|
||||||
.object({
|
|
||||||
url: z.string().optional().describe('OAuth callback URL'),
|
|
||||||
state: z.string().optional().describe('OAuth state parameter'),
|
|
||||||
codeVerifier: z.string().optional().describe('OAuth code verifier (PKCE)'),
|
|
||||||
linkToken: z.string().optional().describe('OAuth link token from prior callback'),
|
|
||||||
})
|
|
||||||
.refine((data) => data.url || data.linkToken, {
|
|
||||||
message: 'Either url or linkToken is required',
|
|
||||||
})
|
|
||||||
.meta({ id: 'OAuthLinkDto' });
|
|
||||||
|
|
||||||
const OAuthConfigSchema = z
|
const OAuthConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
redirectUri: z.string().describe('OAuth redirect URI'),
|
redirectUri: z.string().describe('OAuth redirect URI'),
|
||||||
@@ -161,7 +150,6 @@ export class SessionUnlockDto extends createZodDto(SessionUnlockSchema) {}
|
|||||||
export class PinCodeChangeDto extends createZodDto(PinCodeChangeSchema) {}
|
export class PinCodeChangeDto extends createZodDto(PinCodeChangeSchema) {}
|
||||||
export class ValidateAccessTokenResponseDto extends createZodDto(ValidateAccessTokenResponseSchema) {}
|
export class ValidateAccessTokenResponseDto extends createZodDto(ValidateAccessTokenResponseSchema) {}
|
||||||
export class OAuthCallbackDto extends createZodDto(OAuthCallbackSchema) {}
|
export class OAuthCallbackDto extends createZodDto(OAuthCallbackSchema) {}
|
||||||
export class OAuthLinkDto extends createZodDto(OAuthLinkSchema) {}
|
|
||||||
export class OAuthConfigDto extends createZodDto(OAuthConfigSchema) {}
|
export class OAuthConfigDto extends createZodDto(OAuthConfigSchema) {}
|
||||||
export class OAuthAuthorizeResponseDto extends createZodDto(OAuthAuthorizeResponseSchema) {}
|
export class OAuthAuthorizeResponseDto extends createZodDto(OAuthAuthorizeResponseSchema) {}
|
||||||
export class OAuthBackchannelLogoutDto extends createZodDto(OAuthBackchannelLogoutSchema) {}
|
export class OAuthBackchannelLogoutDto extends createZodDto(OAuthBackchannelLogoutSchema) {}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export class OAuthLinkTokenRepository {
|
|||||||
return this.db.insertInto('oauth_link_token').values(dto).returningAll().executeTakeFirstOrThrow();
|
return this.db.insertInto('oauth_link_token').values(dto).returningAll().executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomic consume: delete and return in one query (single-use guarantee)
|
|
||||||
consumeToken(token: Buffer) {
|
consumeToken(token: Buffer) {
|
||||||
return this.db
|
return this.db
|
||||||
.deleteFrom('oauth_link_token')
|
.deleteFrom('oauth_link_token')
|
||||||
|
|||||||
@@ -88,6 +88,37 @@ describe(AuthService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should link an OAuth account when linkToken is provided', async () => {
|
||||||
|
const user = UserFactory.create({ password: 'immich_password' });
|
||||||
|
const session = SessionFactory.create();
|
||||||
|
mocks.user.getByEmail.mockResolvedValue(user);
|
||||||
|
mocks.session.create.mockResolvedValue(session);
|
||||||
|
mocks.oauthLinkToken.consumeToken.mockResolvedValue({
|
||||||
|
id: 'token-id',
|
||||||
|
oauthSub: 'oauth-sub-123',
|
||||||
|
userEmail: user.email,
|
||||||
|
token: Buffer.from('hashed'),
|
||||||
|
expiresAt: new Date(Date.now() + 600_000),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
mocks.user.update.mockResolvedValue(user);
|
||||||
|
|
||||||
|
await sut.login({ email, password: 'password', linkToken: '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 () => {
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('changePassword', () => {
|
describe('changePassword', () => {
|
||||||
@@ -718,7 +749,7 @@ describe(AuthService.name, () => {
|
|||||||
{},
|
{},
|
||||||
loginDetails,
|
loginDetails,
|
||||||
),
|
),
|
||||||
).rejects.toThrow('oauth_account_link_required');
|
).rejects.toThrow(ForbiddenException);
|
||||||
|
|
||||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||||
@@ -762,7 +793,7 @@ describe(AuthService.name, () => {
|
|||||||
{},
|
{},
|
||||||
loginDetails,
|
loginDetails,
|
||||||
),
|
),
|
||||||
).rejects.toThrow('oauth_account_link_required');
|
).rejects.toThrow(ForbiddenException);
|
||||||
|
|
||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||||
expect(mocks.user.create).not.toHaveBeenCalled();
|
expect(mocks.user.create).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -75,6 +75,21 @@ export class AuthService extends BaseService {
|
|||||||
throw new UnauthorizedException('Incorrect email or password');
|
throw new UnauthorizedException('Incorrect email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dto.linkToken) {
|
||||||
|
const hashedToken = this.cryptoRepository.hashSha256(dto.linkToken);
|
||||||
|
const record = await this.oauthLinkTokenRepository.consumeToken(hashedToken);
|
||||||
|
if (!record) {
|
||||||
|
throw new BadRequestException('Invalid or expired link token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicate = await this.userRepository.getByOAuthId(record.oauthSub);
|
||||||
|
if (duplicate && duplicate.id !== user.id) {
|
||||||
|
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepository.update(user.id, { oauthId: record.oauthSub });
|
||||||
|
}
|
||||||
|
|
||||||
return this.createLoginResponse(user, details);
|
return this.createLoginResponse(user, details);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,9 +345,9 @@ export class AuthService extends BaseService {
|
|||||||
token: hashedToken,
|
token: hashedToken,
|
||||||
oauthSub: profile.sub,
|
oauthSub: profile.sub,
|
||||||
userEmail: emailUser.email,
|
userEmail: emailUser.email,
|
||||||
expiresAt: new Date(Date.now() + 10 * 60 * 1000),
|
expiresAt: DateTime.now().plus({ minutes: 10 }).toJSDate(),
|
||||||
});
|
});
|
||||||
throw new BadRequestException({
|
throw new ForbiddenException({
|
||||||
message: 'oauth_account_link_required',
|
message: 'oauth_account_link_required',
|
||||||
userEmail: emailUser.email,
|
userEmail: emailUser.email,
|
||||||
linkToken: plainToken,
|
linkToken: plainToken,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
getBaseUrl,
|
getBaseUrl,
|
||||||
getPeopleThumbnailPath,
|
getPeopleThumbnailPath,
|
||||||
getUserProfileImagePath,
|
getUserProfileImagePath,
|
||||||
linkOAuthAccount,
|
|
||||||
startOAuth,
|
startOAuth,
|
||||||
unlinkOAuthAccount,
|
unlinkOAuthAccount,
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
@@ -293,9 +292,6 @@ export const oauth = {
|
|||||||
login: (location: Location) => {
|
login: (location: Location) => {
|
||||||
return finishOAuth({ oAuthCallbackDto: { url: location.href } });
|
return finishOAuth({ oAuthCallbackDto: { url: location.href } });
|
||||||
},
|
},
|
||||||
link: (location: Location) => {
|
|
||||||
return linkOAuthAccount({ oAuthLinkDto: { url: location.href } });
|
|
||||||
},
|
|
||||||
unlink: () => {
|
unlink: () => {
|
||||||
return unlinkOAuthAccount();
|
return unlinkOAuthAccount();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { getServerErrorMessage } from '$lib/utils/handle-error';
|
import { getServerErrorMessage } from '$lib/utils/handle-error';
|
||||||
import { linkOAuthAccount, login } from '@immich/sdk';
|
import { login } from '@immich/sdk';
|
||||||
import { Alert, Button, Field, Input, PasswordInput, Stack, toastManager } from '@immich/ui';
|
import { Alert, Button, Field, Input, PasswordInput, Stack, toastManager } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
@@ -17,24 +17,22 @@
|
|||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
|
let linkToken = $state(data.linkToken);
|
||||||
let email = $state(data.email || authManager.user?.email || '');
|
let email = $state(data.email || authManager.user?.email || '');
|
||||||
let password = $state('');
|
let password = $state('');
|
||||||
let errorMessage = $state('');
|
let errorMessage = $state('');
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
// Strip sensitive params from URL after reading — keeps linkToken out of browser history
|
goto(Route.authLink(), { replaceState: true });
|
||||||
history.replaceState(null, '', Route.authLink());
|
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
loading = true;
|
loading = true;
|
||||||
const user = await login({ loginCredentialDto: { email, password } });
|
const user = await login({ loginCredentialDto: { email, password, linkToken } });
|
||||||
eventManager.emit('AuthLogin', user);
|
eventManager.emit('AuthLogin', user);
|
||||||
|
await authManager.refresh();
|
||||||
const response = await linkOAuthAccount({ oAuthLinkDto: { linkToken: data.linkToken } });
|
|
||||||
authManager.setUser(response);
|
|
||||||
toastManager.primary($t('linked_oauth_account'));
|
toastManager.primary($t('linked_oauth_account'));
|
||||||
await goto(Route.photos(), { invalidateAll: true });
|
await goto(Route.photos(), { invalidateAll: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user