diff --git a/docs/docs/features/img/password-login-settings.png b/docs/docs/features/img/password-login-settings.png new file mode 100644 index 0000000000..2c87081db5 Binary files /dev/null and b/docs/docs/features/img/password-login-settings.png differ diff --git a/docs/docs/features/img/user-management-update.png b/docs/docs/features/img/user-management-update.png new file mode 100644 index 0000000000..428359e573 Binary files /dev/null and b/docs/docs/features/img/user-management-update.png differ diff --git a/docs/docs/features/oauth.md b/docs/docs/features/oauth.md index 406772c935..b4bf97063a 100644 --- a/docs/docs/features/oauth.md +++ b/docs/docs/features/oauth.md @@ -59,16 +59,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured Once you have a new OAuth client application configured, Immich can be configured using the Administration Settings page, available on the web (Administration -> Settings). -| Setting | Type | Default | Description | -| ---------------------------- | ------- | -------------------- | ------------------------------------------------------------------------- | -| Enabled | boolean | false | Enable/disable OAuth | -| Issuer URL | URL | (required) | Required. Self-discovery URL for client (from previous step) | -| Client ID | string | (required) | Required. Client ID (from previous step) | -| Client secret | string | (required) | Required. Client Secret (previous step) | -| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) | -| Button text | string | Login with OAuth | Text for the OAuth button on the web | -| Auto register | boolean | true | When true, will automatically register a user the first time they sign in | -| Mobile Redirect URI Override | URL | (empty) | Http(s) alternative mobile redirect URI | +| Setting | Type | Default | Description | +| ---------------------------------------------------- | ------- | -------------------- | ----------------------------------------------------------------------------------- | +| Enabled | boolean | false | Enable/disable OAuth | +| Issuer URL | URL | (required) | Required. Self-discovery URL for client (from previous step) | +| Client ID | string | (required) | Required. Client ID (from previous step) | +| Client Secret | string | (required) | Required. Client Secret (previous step) | +| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) | +| Button Text | string | Login with OAuth | Text for the OAuth button on the web | +| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in | +| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process | +| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI | :::info The Issuer URL should look something like the following, and return a valid json document. @@ -79,6 +80,10 @@ The Issuer URL should look something like the following, and return a valid json The `.well-known/openid-configuration` part of the url is optional and will be automatically added during discovery. ::: +## Auto Launch + +When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`. + ## Mobile Redirect URI The redirect URI for the mobile app is `app.immich:/`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following: diff --git a/docs/docs/features/password-login.md b/docs/docs/features/password-login.md new file mode 100644 index 0000000000..76ab057e89 --- /dev/null +++ b/docs/docs/features/password-login.md @@ -0,0 +1,32 @@ +# Password Login + +An overview of password login and related settings for Immich. + +## Enable/Disable + +Immich supports password login, which is enabled by default. The preferred way to disable it is via the [Administration Page](#administration-page), although it can also be changed via a [Server Command](#server-command) as well. + +### Administration Page + +To toggle the password login setting via the web, navigate to the "Administration", expand "Password Authentication", toggle the "Enabled" switch, and press "Save". + +![Password Login Settings](./img/password-login-settings.png) + +### Server Command + +There are two [Server Commands](/docs/features/server-commands.md) for password login: + +1. `enable-password-login` +2. `disable-password-login` + +See [Server Commands](/docs/features/server-commands.md) for more details about how to run them. + +## Password Reset + +### Admin + +To reset the administrator password, use the `reset-admin-password` [Server Command](/docs/features/server-commands.md). + +### User + +Immich does not currently support self-service password reset. However, the administration can reset passwords for other users. See [User Management: Password Reset](/docs/features/user-management.mdx#password-reset) for more information about how to do this. diff --git a/docs/docs/features/server-commands.md b/docs/docs/features/server-commands.md index 0a41099e3a..d8c017cade 100644 --- a/docs/docs/features/server-commands.md +++ b/docs/docs/features/server-commands.md @@ -1,21 +1,39 @@ # Server Commands -The `immich-server` docker image comes preinstalled with an administrative CLI that supports the following commands: +The `immich-server` docker image comes preinstalled with an administrative CLI (`immich`) that supports the following commands: -| Command | Description | -| ----------------------------- | ------------------------------------- | -| `immich help` | Display help | -| `immich reset-admin-password` | Reset the password for the admin user | +| Command | Description | +| ------------------------ | ------------------------------------- | +| `help` | Display help | +| `reset-admin-password` | Reset the password for the admin user | +| `disable-password-login` | Disable password login | +| `enable-password-login` | Enable password login | ## How to run a command -To run a command, connect to the container and then execute it. For example: +To run a command, connect to the container and then execute it by running `immich `. -```bash -docker exec -it immich-server_1 sh +## Examples + +```bash title="Reset Admin Password" +docker exec -it immich_server sh /usr/src/app$ immich reset-admin-password ? Please choose a new password (optional) immich-is-awesome-unlike-this-password New password: immich-is-awesome-unlike-this-password ``` + +```bash title="Disable Password Login" +docker exec -it immich_server sh + +/usr/src/app$ immich disable-password-login +Password login has been disabled. +``` + +```bash title="Enable Password Login" +docker exec -it immich_server sh + +/usr/src/app$ immich enable-password-login +Password login has been enabled. +``` diff --git a/docs/docs/features/user-management.mdx b/docs/docs/features/user-management.mdx index a3d977c06b..10715c5a95 100644 --- a/docs/docs/features/user-management.mdx +++ b/docs/docs/features/user-management.mdx @@ -16,3 +16,9 @@ Immich supports multiple users, each with their own library. ## Delete a User If you need to remove a user from Immich, head to "Administration", where users can be scheduled for deletion. The user account will immediately become disabled and their library and all associated data will be removed after 7 days. + +## Password Reset + +To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to "password" and they have to change it next time the sign in. + +![Reset Password](./img/user-management-update.png) diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 76099cd883..d68e7e9b31 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -76,6 +76,7 @@ doc/SystemConfigApi.md doc/SystemConfigDto.md doc/SystemConfigFFmpegDto.md doc/SystemConfigOAuthDto.md +doc/SystemConfigPasswordLoginDto.md doc/SystemConfigStorageTemplateDto.md doc/SystemConfigTemplateStorageOptionDto.md doc/TagApi.md @@ -178,6 +179,7 @@ lib/model/smart_info_response_dto.dart lib/model/system_config_dto.dart lib/model/system_config_f_fmpeg_dto.dart lib/model/system_config_o_auth_dto.dart +lib/model/system_config_password_login_dto.dart lib/model/system_config_storage_template_dto.dart lib/model/system_config_template_storage_option_dto.dart lib/model/tag_response_dto.dart @@ -267,6 +269,7 @@ test/system_config_api_test.dart test/system_config_dto_test.dart test/system_config_f_fmpeg_dto_test.dart test/system_config_o_auth_dto_test.dart +test/system_config_password_login_dto_test.dart test/system_config_storage_template_dto_test.dart test/system_config_template_storage_option_dto_test.dart test/tag_api_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 786043e174..5ff0099b03 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/OAuthConfigResponseDto.md b/mobile/openapi/doc/OAuthConfigResponseDto.md index 8d6c3a41e8..ae1d42c12f 100644 Binary files a/mobile/openapi/doc/OAuthConfigResponseDto.md and b/mobile/openapi/doc/OAuthConfigResponseDto.md differ diff --git a/mobile/openapi/doc/SharedLinkResponseDto.md b/mobile/openapi/doc/SharedLinkResponseDto.md index b27cc6dbc2..c11a1a4896 100644 Binary files a/mobile/openapi/doc/SharedLinkResponseDto.md and b/mobile/openapi/doc/SharedLinkResponseDto.md differ diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index 19209682fe..8ad2bfb9a3 100644 Binary files a/mobile/openapi/doc/SystemConfigDto.md and b/mobile/openapi/doc/SystemConfigDto.md differ diff --git a/mobile/openapi/doc/SystemConfigOAuthDto.md b/mobile/openapi/doc/SystemConfigOAuthDto.md index dfdaa67126..745b13b79a 100644 Binary files a/mobile/openapi/doc/SystemConfigOAuthDto.md and b/mobile/openapi/doc/SystemConfigOAuthDto.md differ diff --git a/mobile/openapi/doc/SystemConfigPasswordLoginDto.md b/mobile/openapi/doc/SystemConfigPasswordLoginDto.md new file mode 100644 index 0000000000..682a3c6447 Binary files /dev/null and b/mobile/openapi/doc/SystemConfigPasswordLoginDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index e2ea9592a8..595ca69b51 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 1e2ef461b2..3a916af671 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/o_auth_config_response_dto.dart b/mobile/openapi/lib/model/o_auth_config_response_dto.dart index 29cdda644c..db6c9c9e92 100644 Binary files a/mobile/openapi/lib/model/o_auth_config_response_dto.dart and b/mobile/openapi/lib/model/o_auth_config_response_dto.dart differ diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index 3c51d90d2d..aad8972d2e 100644 Binary files a/mobile/openapi/lib/model/shared_link_response_dto.dart and b/mobile/openapi/lib/model/shared_link_response_dto.dart differ diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 22701819a9..a4e9760512 100644 Binary files a/mobile/openapi/lib/model/system_config_dto.dart and b/mobile/openapi/lib/model/system_config_dto.dart differ diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index d291b501d8..d0fb195ccc 100644 Binary files a/mobile/openapi/lib/model/system_config_o_auth_dto.dart and b/mobile/openapi/lib/model/system_config_o_auth_dto.dart differ diff --git a/mobile/openapi/lib/model/system_config_password_login_dto.dart b/mobile/openapi/lib/model/system_config_password_login_dto.dart new file mode 100644 index 0000000000..f5562800a9 Binary files /dev/null and b/mobile/openapi/lib/model/system_config_password_login_dto.dart differ diff --git a/mobile/openapi/test/o_auth_config_response_dto_test.dart b/mobile/openapi/test/o_auth_config_response_dto_test.dart index 1bb6cbc89c..309324f884 100644 Binary files a/mobile/openapi/test/o_auth_config_response_dto_test.dart and b/mobile/openapi/test/o_auth_config_response_dto_test.dart differ diff --git a/mobile/openapi/test/shared_link_response_dto_test.dart b/mobile/openapi/test/shared_link_response_dto_test.dart index 46778bfa71..de19ef71b8 100644 Binary files a/mobile/openapi/test/shared_link_response_dto_test.dart and b/mobile/openapi/test/shared_link_response_dto_test.dart differ diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index b4a3172ce3..e44406f7ec 100644 Binary files a/mobile/openapi/test/system_config_dto_test.dart and b/mobile/openapi/test/system_config_dto_test.dart differ diff --git a/mobile/openapi/test/system_config_o_auth_dto_test.dart b/mobile/openapi/test/system_config_o_auth_dto_test.dart index 744f55dd04..ca5fadad4a 100644 Binary files a/mobile/openapi/test/system_config_o_auth_dto_test.dart and b/mobile/openapi/test/system_config_o_auth_dto_test.dart differ diff --git a/mobile/openapi/test/system_config_password_login_dto_test.dart b/mobile/openapi/test/system_config_password_login_dto_test.dart new file mode 100644 index 0000000000..a8d87d1547 Binary files /dev/null and b/mobile/openapi/test/system_config_password_login_dto_test.dart differ diff --git a/server/apps/cli/src/app.module.ts b/server/apps/cli/src/app.module.ts index b529c7b136..b1b0de7ec8 100644 --- a/server/apps/cli/src/app.module.ts +++ b/server/apps/cli/src/app.module.ts @@ -1,10 +1,16 @@ -import { DatabaseModule, UserEntity } from '@app/database'; +import { DatabaseModule, SystemConfigEntity, UserEntity } from '@app/database'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from './commands/password-login'; import { PromptPasswordQuestions, ResetAdminPasswordCommand } from './commands/reset-admin-password.command'; @Module({ - imports: [DatabaseModule, TypeOrmModule.forFeature([UserEntity])], - providers: [ResetAdminPasswordCommand, PromptPasswordQuestions], + imports: [DatabaseModule, TypeOrmModule.forFeature([UserEntity, SystemConfigEntity])], + providers: [ + ResetAdminPasswordCommand, + PromptPasswordQuestions, + EnablePasswordLoginCommand, + DisablePasswordLoginCommand, + ], }) export class AppModule {} diff --git a/server/apps/cli/src/commands/password-login.ts b/server/apps/cli/src/commands/password-login.ts new file mode 100644 index 0000000000..a2a07a33e2 --- /dev/null +++ b/server/apps/cli/src/commands/password-login.ts @@ -0,0 +1,39 @@ +import { SystemConfigEntity, SystemConfigKey } from '@app/database'; +import { InjectRepository } from '@nestjs/typeorm'; +import axios from 'axios'; +import { Command, CommandRunner } from 'nest-commander'; +import { Repository } from 'typeorm'; + +@Command({ + name: 'enable-password-login', + description: 'Enable password login', +}) +export class EnablePasswordLoginCommand extends CommandRunner { + constructor( + @InjectRepository(SystemConfigEntity) private repository: Repository, // + ) { + super(); + } + + async run(): Promise { + await this.repository.delete({ key: SystemConfigKey.PASSWORD_LOGIN_ENABLED }); + await axios.post('http://localhost:3001/refresh-config'); + console.log('Password login has been enabled.'); + } +} + +@Command({ + name: 'disable-password-login', + description: 'Disable password login', +}) +export class DisablePasswordLoginCommand extends CommandRunner { + constructor(@InjectRepository(SystemConfigEntity) private repository: Repository) { + super(); + } + + async run(): Promise { + await this.repository.save({ key: SystemConfigKey.PASSWORD_LOGIN_ENABLED, value: false }); + await axios.post('http://localhost:3001/refresh-config'); + console.log('Password login has been disabled.'); + } +} diff --git a/server/apps/immich/src/api-v1/album/album.service.spec.ts b/server/apps/immich/src/api-v1/album/album.service.spec.ts index 61633d7c90..a14c6d2ba2 100644 --- a/server/apps/immich/src/api-v1/album/album.service.spec.ts +++ b/server/apps/immich/src/api-v1/album/album.service.spec.ts @@ -136,6 +136,8 @@ describe('Album service', () => { getById: jest.fn(), getByKey: jest.fn(), save: jest.fn(), + hasAssetAccess: jest.fn(), + getByIdAndUserId: jest.fn(), }; downloadServiceMock = { diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index ce38fc84e5..0778f80688 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -130,7 +130,6 @@ describe('AssetService', () => { getAssetWithNoSmartInfo: jest.fn(), getExistingAssets: jest.fn(), countByIdAndUser: jest.fn(), - getSharePermission: jest.fn(), }; downloadServiceMock = { @@ -144,6 +143,8 @@ describe('AssetService', () => { getByKey: jest.fn(), remove: jest.fn(), save: jest.fn(), + hasAssetAccess: jest.fn(), + getByIdAndUserId: jest.fn(), }; sui = new AssetService( diff --git a/server/apps/immich/src/api-v1/auth/auth.module.ts b/server/apps/immich/src/api-v1/auth/auth.module.ts index 4a06f0ae8a..f1d93f067f 100644 --- a/server/apps/immich/src/api-v1/auth/auth.module.ts +++ b/server/apps/immich/src/api-v1/auth/auth.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { ImmichConfigModule } from '@app/immich-config'; import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; import { OAuthModule } from '../oauth/oauth.module'; import { UserModule } from '../user/user.module'; @@ -6,7 +7,7 @@ import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; @Module({ - imports: [UserModule, ImmichJwtModule, OAuthModule], + imports: [UserModule, ImmichJwtModule, OAuthModule, ImmichConfigModule], controllers: [AuthController], providers: [AuthService], }) diff --git a/server/apps/immich/src/api-v1/auth/auth.service.spec.ts b/server/apps/immich/src/api-v1/auth/auth.service.spec.ts index b84420d1b2..9234171372 100644 --- a/server/apps/immich/src/api-v1/auth/auth.service.spec.ts +++ b/server/apps/immich/src/api-v1/auth/auth.service.spec.ts @@ -1,6 +1,8 @@ import { UserEntity } from '@app/database'; import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; +import { SystemConfig } from '@app/database/entities/system-config.entity'; +import { ImmichConfigService } from '@app/immich-config'; import { AuthType } from '../../constants/jwt.constant'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { OAuthService } from '../oauth/oauth.service'; @@ -16,6 +18,19 @@ const fixtures = { }, }; +const config = { + enabled: { + passwordLogin: { + enabled: true, + }, + } as SystemConfig, + disabled: { + passwordLogin: { + enabled: false, + }, + } as SystemConfig, +}; + const CLIENT_IP = '127.0.0.1'; jest.mock('bcrypt'); @@ -35,6 +50,7 @@ describe('AuthService', () => { let sut: AuthService; let userRepositoryMock: jest.Mocked; let immichJwtServiceMock: jest.Mocked; + let immichConfigServiceMock: jest.Mocked; let oauthServiceMock: jest.Mocked; let compare: jest.Mock; @@ -71,14 +87,40 @@ describe('AuthService', () => { getLogoutEndpoint: jest.fn(), } as unknown as jest.Mocked; - sut = new AuthService(oauthServiceMock, immichJwtServiceMock, userRepositoryMock); + immichConfigServiceMock = { + config$: { subscribe: jest.fn() }, + } as unknown as jest.Mocked; + + sut = new AuthService( + oauthServiceMock, + immichJwtServiceMock, + userRepositoryMock, + immichConfigServiceMock, + config.enabled, + ); }); it('should be defined', () => { expect(sut).toBeDefined(); }); + it('should subscribe to config changes', async () => { + expect(immichConfigServiceMock.config$.subscribe).toHaveBeenCalled(); + }); + describe('login', () => { + it('should throw an error if password login is disabled', async () => { + sut = new AuthService( + oauthServiceMock, + immichJwtServiceMock, + userRepositoryMock, + immichConfigServiceMock, + config.disabled, + ); + + await expect(sut.login(fixtures.login, CLIENT_IP)).rejects.toBeInstanceOf(UnauthorizedException); + }); + it('should check the user exists', async () => { userRepositoryMock.getByEmail.mockResolvedValue(null); await expect(sut.login(fixtures.login, CLIENT_IP)).rejects.toBeInstanceOf(BadRequestException); @@ -170,7 +212,7 @@ describe('AuthService', () => { it('should return the default redirect', async () => { await expect(sut.logout(AuthType.PASSWORD)).resolves.toEqual({ successful: true, - redirectUri: '/auth/login', + redirectUri: '/auth/login?autoLaunch=0', }); expect(oauthServiceMock.getLogoutEndpoint).not.toHaveBeenCalled(); }); diff --git a/server/apps/immich/src/api-v1/auth/auth.service.ts b/server/apps/immich/src/api-v1/auth/auth.service.ts index 6a671decb9..1721c7d31a 100644 --- a/server/apps/immich/src/api-v1/auth/auth.service.ts +++ b/server/apps/immich/src/api-v1/auth/auth.service.ts @@ -20,6 +20,8 @@ import { LoginResponseDto } from './response-dto/login-response.dto'; import { LogoutResponseDto } from './response-dto/logout-response.dto'; import { OAuthService } from '../oauth/oauth.service'; import { UserCore } from '../user/user.core'; +import { ImmichConfigService, INITIAL_SYSTEM_CONFIG } from '@app/immich-config'; +import { SystemConfig } from '@app/database/entities/system-config.entity'; @Injectable() export class AuthService { @@ -30,11 +32,18 @@ export class AuthService { private oauthService: OAuthService, private immichJwtService: ImmichJwtService, @Inject(IUserRepository) userRepository: IUserRepository, + private configService: ImmichConfigService, + @Inject(INITIAL_SYSTEM_CONFIG) private config: SystemConfig, ) { this.userCore = new UserCore(userRepository); + this.configService.config$.subscribe((config) => (this.config = config)); } public async login(loginCredential: LoginCredentialDto, clientIp: string): Promise { + if (!this.config.passwordLogin.enabled) { + throw new UnauthorizedException('Password login has been disabled'); + } + let user = await this.userCore.getByEmail(loginCredential.email, true); if (user) { @@ -60,7 +69,7 @@ export class AuthService { } } - return { successful: true, redirectUri: '/auth/login' }; + return { successful: true, redirectUri: '/auth/login?autoLaunch=0' }; } public async changePassword(authUser: AuthUserDto, dto: ChangePasswordDto) { diff --git a/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts b/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts index 1e931a80ac..645c49ef5e 100644 --- a/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts +++ b/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts @@ -17,29 +17,37 @@ const config = { enabled: false, buttonText: 'OAuth', issuerUrl: 'http://issuer,', + autoLaunch: false, }, + passwordLogin: { enabled: true }, } as SystemConfig, enabled: { oauth: { enabled: true, autoRegister: true, buttonText: 'OAuth', + autoLaunch: false, }, + passwordLogin: { enabled: true }, } as SystemConfig, noAutoRegister: { oauth: { enabled: true, autoRegister: false, + autoLaunch: false, }, + passwordLogin: { enabled: true }, } as SystemConfig, override: { oauth: { enabled: true, autoRegister: true, + autoLaunch: false, buttonText: 'OAuth', mobileOverrideEnabled: true, mobileRedirectUri: 'http://mobile-redirect', }, + passwordLogin: { enabled: true }, } as SystemConfig, }; @@ -124,7 +132,6 @@ describe('OAuthService', () => { immichConfigServiceMock = { config$: { subscribe: jest.fn() }, - getConfig: jest.fn().mockResolvedValue({ oauth: { enabled: false } }), } as unknown as jest.Mocked; sut = new OAuthService(immichJwtServiceMock, immichConfigServiceMock, userRepositoryMock, config.disabled); @@ -136,7 +143,10 @@ describe('OAuthService', () => { describe('generateConfig', () => { it('should work when oauth is not configured', async () => { - await expect(sut.generateConfig({ redirectUri: 'http://callback' })).resolves.toEqual({ enabled: false }); + await expect(sut.generateConfig({ redirectUri: 'http://callback' })).resolves.toEqual({ + enabled: false, + passwordLoginEnabled: true, + }); }); it('should generate the config', async () => { @@ -145,6 +155,8 @@ describe('OAuthService', () => { enabled: true, buttonText: 'OAuth', url: 'http://authorization-url', + autoLaunch: false, + passwordLoginEnabled: true, }); }); }); diff --git a/server/apps/immich/src/api-v1/oauth/oauth.service.ts b/server/apps/immich/src/api-v1/oauth/oauth.service.ts index 8c62f8f623..6097dcd6ff 100644 --- a/server/apps/immich/src/api-v1/oauth/oauth.service.ts +++ b/server/apps/immich/src/api-v1/oauth/oauth.service.ts @@ -39,19 +39,24 @@ export class OAuthService { } public async generateConfig(dto: OAuthConfigDto): Promise { - const { enabled, scope, buttonText } = this.config.oauth; - const redirectUri = this.normalize(dto.redirectUri); + const response = { + enabled: this.config.oauth.enabled, + passwordLoginEnabled: this.config.passwordLogin.enabled, + }; - if (!enabled) { - return { enabled: false }; + if (!response.enabled) { + return response; } + const { scope, buttonText, autoLaunch } = this.config.oauth; + const redirectUri = this.normalize(dto.redirectUri); const url = (await this.getClient()).authorizationUrl({ redirect_uri: redirectUri, scope, state: generators.state(), }); - return { enabled: true, buttonText, url }; + + return { ...response, buttonText, url, autoLaunch }; } public async login(dto: OAuthCallbackDto): Promise { diff --git a/server/apps/immich/src/api-v1/oauth/response-dto/oauth-config-response.dto.ts b/server/apps/immich/src/api-v1/oauth/response-dto/oauth-config-response.dto.ts index 6dc480866e..c239405fc0 100644 --- a/server/apps/immich/src/api-v1/oauth/response-dto/oauth-config-response.dto.ts +++ b/server/apps/immich/src/api-v1/oauth/response-dto/oauth-config-response.dto.ts @@ -1,12 +1,7 @@ -import { ApiResponseProperty } from '@nestjs/swagger'; - export class OAuthConfigResponseDto { - @ApiResponseProperty() enabled!: boolean; - - @ApiResponseProperty() + passwordLoginEnabled!: boolean; url?: string; - - @ApiResponseProperty() buttonText?: string; + autoLaunch?: boolean; } diff --git a/server/apps/immich/src/api-v1/system-config/dto/system-config-oauth.dto.ts b/server/apps/immich/src/api-v1/system-config/dto/system-config-oauth.dto.ts index 722e6c199b..6cc4590744 100644 --- a/server/apps/immich/src/api-v1/system-config/dto/system-config-oauth.dto.ts +++ b/server/apps/immich/src/api-v1/system-config/dto/system-config-oauth.dto.ts @@ -31,6 +31,9 @@ export class SystemConfigOAuthDto { @IsBoolean() autoRegister!: boolean; + @IsBoolean() + autoLaunch!: boolean; + @IsBoolean() mobileOverrideEnabled!: boolean; diff --git a/server/apps/immich/src/api-v1/system-config/dto/system-config-password-login.dto.ts b/server/apps/immich/src/api-v1/system-config/dto/system-config-password-login.dto.ts new file mode 100644 index 0000000000..119de65f61 --- /dev/null +++ b/server/apps/immich/src/api-v1/system-config/dto/system-config-password-login.dto.ts @@ -0,0 +1,6 @@ +import { IsBoolean } from 'class-validator'; + +export class SystemConfigPasswordLoginDto { + @IsBoolean() + enabled!: boolean; +} diff --git a/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts b/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts index 498d1e7b5b..1bb2e736f8 100644 --- a/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts +++ b/server/apps/immich/src/api-v1/system-config/dto/system-config.dto.ts @@ -2,6 +2,7 @@ import { SystemConfig } from '@app/database'; import { ValidateNested } from 'class-validator'; import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; import { SystemConfigOAuthDto } from './system-config-oauth.dto'; +import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto'; import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; export class SystemConfigDto { @@ -11,6 +12,9 @@ export class SystemConfigDto { @ValidateNested() oauth!: SystemConfigOAuthDto; + @ValidateNested() + passwordLogin!: SystemConfigPasswordLoginDto; + @ValidateNested() storageTemplate!: SystemConfigStorageTemplateDto; } diff --git a/server/apps/immich/src/app.controller.ts b/server/apps/immich/src/app.controller.ts index b657baf8c6..44207a609d 100644 --- a/server/apps/immich/src/app.controller.ts +++ b/server/apps/immich/src/app.controller.ts @@ -1,3 +1,15 @@ -import { Controller } from '@nestjs/common'; +import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiExcludeEndpoint } from '@nestjs/swagger'; +import { ImmichConfigService } from '@app/immich-config'; + @Controller() -export class AppController {} +export class AppController { + constructor(private configService: ImmichConfigService) {} + + @ApiExcludeEndpoint() + @Post('refresh-config') + @HttpCode(HttpStatus.OK) + public reloadConfig() { + return this.configService.refreshConfig(); + } +} diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts index ced895fec5..46db47b402 100644 --- a/server/apps/immich/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -3,6 +3,7 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { UserModule } from './api-v1/user/user.module'; import { AssetModule } from './api-v1/asset/asset.module'; import { AuthModule } from './api-v1/auth/auth.module'; +import { APIKeyModule } from './api-v1/api-key/api-key.module'; import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module'; import { DeviceInfoModule } from './api-v1/device-info/device-info.module'; import { ConfigModule } from '@nestjs/config'; @@ -19,8 +20,8 @@ import { JobModule } from './api-v1/job/job.module'; import { SystemConfigModule } from './api-v1/system-config/system-config.module'; import { OAuthModule } from './api-v1/oauth/oauth.module'; import { TagModule } from './api-v1/tag/tag.module'; +import { ImmichConfigModule } from '@app/immich-config'; import { ShareModule } from './api-v1/share/share.module'; -import { APIKeyModule } from './api-v1/api-key/api-key.module'; @Module({ imports: [ @@ -37,6 +38,7 @@ import { APIKeyModule } from './api-v1/api-key/api-key.module'; OAuthModule, ImmichJwtModule, + ImmichConfigModule, DeviceInfoModule, diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts b/server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts index bf4aa8f9e7..8a35800072 100644 --- a/server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts +++ b/server/apps/immich/src/modules/immich-jwt/strategies/api-key.strategy.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; -import { AuthUserDto } from 'apps/immich/src/decorators/auth-user.decorator'; import { IStrategyOptions, Strategy } from 'passport-http-header-strategy'; import { APIKeyService } from '../../../api-v1/api-key/api-key.service'; +import { AuthUserDto } from '../../../decorators/auth-user.decorator'; export const API_KEY_STRATEGY = 'api-key'; diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts index 916e718e2c..551aa3a796 100644 --- a/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts +++ b/server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts @@ -1,13 +1,13 @@ +import { UserEntity } from '@app/database'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt'; import { Repository } from 'typeorm'; import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto'; -import { UserEntity } from '@app/database'; import { jwtSecret } from '../../../constants/jwt.constant'; +import { AuthUserDto } from '../../../decorators/auth-user.decorator'; import { ImmichJwtService } from '../immich-jwt.service'; -import { AuthUserDto } from 'apps/immich/src/decorators/auth-user.decorator'; export const JWT_STRATEGY = 'jwt'; diff --git a/server/apps/immich/src/modules/immich-jwt/strategies/public-share.strategy.ts b/server/apps/immich/src/modules/immich-jwt/strategies/public-share.strategy.ts index 41393e294d..f3e79eef5f 100644 --- a/server/apps/immich/src/modules/immich-jwt/strategies/public-share.strategy.ts +++ b/server/apps/immich/src/modules/immich-jwt/strategies/public-share.strategy.ts @@ -2,10 +2,10 @@ import { UserEntity } from '@app/database'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; -import { ShareService } from '../../../api-v1/share/share.service'; import { IStrategyOptions, Strategy } from 'passport-http-header-strategy'; import { Repository } from 'typeorm'; -import { AuthUserDto } from 'apps/immich/src/decorators/auth-user.decorator'; +import { ShareService } from '../../../api-v1/share/share.service'; +import { AuthUserDto } from '../../../decorators/auth-user.decorator'; export const PUBLIC_SHARE_STRATEGY = 'public-share'; diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 92fad332a3..eb36992a79 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -3936,20 +3936,24 @@ "type": "object", "properties": { "enabled": { - "type": "boolean", - "readOnly": true + "type": "boolean" + }, + "passwordLoginEnabled": { + "type": "boolean" }, "url": { - "type": "string", - "readOnly": true + "type": "string" }, "buttonText": { - "type": "string", - "readOnly": true + "type": "string" + }, + "autoLaunch": { + "type": "boolean" } }, "required": [ - "enabled" + "enabled", + "passwordLoginEnabled" ] }, "OAuthCallbackDto": { @@ -4334,6 +4338,9 @@ "autoRegister": { "type": "boolean" }, + "autoLaunch": { + "type": "boolean" + }, "mobileOverrideEnabled": { "type": "boolean" }, @@ -4349,10 +4356,22 @@ "scope", "buttonText", "autoRegister", + "autoLaunch", "mobileOverrideEnabled", "mobileRedirectUri" ] }, + "SystemConfigPasswordLoginDto": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ] + }, "SystemConfigStorageTemplateDto": { "type": "object", "properties": { @@ -4373,6 +4392,9 @@ "oauth": { "$ref": "#/components/schemas/SystemConfigOAuthDto" }, + "passwordLogin": { + "$ref": "#/components/schemas/SystemConfigPasswordLoginDto" + }, "storageTemplate": { "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" } @@ -4380,6 +4402,7 @@ "required": [ "ffmpeg", "oauth", + "passwordLogin", "storageTemplate" ] }, diff --git a/server/libs/database/src/entities/system-config.entity.ts b/server/libs/database/src/entities/system-config.entity.ts index 40378b5bcf..de9280e4e5 100644 --- a/server/libs/database/src/entities/system-config.entity.ts +++ b/server/libs/database/src/entities/system-config.entity.ts @@ -1,7 +1,7 @@ import { Column, Entity, PrimaryColumn } from 'typeorm'; @Entity('system_config') -export class SystemConfigEntity { +export class SystemConfigEntity { @PrimaryColumn() key!: SystemConfigKey; @@ -23,10 +23,12 @@ export enum SystemConfigKey { OAUTH_CLIENT_ID = 'oauth.clientId', OAUTH_CLIENT_SECRET = 'oauth.clientSecret', OAUTH_SCOPE = 'oauth.scope', + OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch', OAUTH_BUTTON_TEXT = 'oauth.buttonText', OAUTH_AUTO_REGISTER = 'oauth.autoRegister', OAUTH_MOBILE_OVERRIDE_ENABLED = 'oauth.mobileOverrideEnabled', OAUTH_MOBILE_REDIRECT_URI = 'oauth.mobileRedirectUri', + PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled', STORAGE_TEMPLATE = 'storageTemplate.template', } @@ -46,9 +48,13 @@ export interface SystemConfig { scope: string; buttonText: string; autoRegister: boolean; + autoLaunch: boolean; mobileOverrideEnabled: boolean; mobileRedirectUri: string; }; + passwordLogin: { + enabled: boolean; + }; storageTemplate: { template: string; }; diff --git a/server/libs/immich-config/src/immich-config.service.ts b/server/libs/immich-config/src/immich-config.service.ts index 6cb37e2be8..856629dde1 100644 --- a/server/libs/immich-config/src/immich-config.service.ts +++ b/server/libs/immich-config/src/immich-config.service.ts @@ -25,6 +25,10 @@ const defaults: SystemConfig = Object.freeze({ scope: 'openid email profile', buttonText: 'Login with OAuth', autoRegister: true, + autoLaunch: false, + }, + passwordLogin: { + enabled: true, }, storageTemplate: { diff --git a/web/package.json b/web/package.json index fd53e50eaa..a67c5103fb 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,7 @@ "build": "vite build", "package": "svelte-kit package", "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings", + "check": "svelte-check --no-tsconfig --fail-on-warnings --ignore \"src/api/open-api\"", "check:watch": "npm run check -- --watch", "check:code": "npm run format && npm run lint && npm run check", "check:all": "npm run check:code && npm test", diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 72e819edf0..4627973a43 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1377,6 +1377,12 @@ export interface OAuthConfigResponseDto { * @memberof OAuthConfigResponseDto */ 'enabled': boolean; + /** + * + * @type {boolean} + * @memberof OAuthConfigResponseDto + */ + 'passwordLoginEnabled': boolean; /** * * @type {string} @@ -1389,6 +1395,12 @@ export interface OAuthConfigResponseDto { * @memberof OAuthConfigResponseDto */ 'buttonText'?: string; + /** + * + * @type {boolean} + * @memberof OAuthConfigResponseDto + */ + 'autoLaunch'?: boolean; } /** * @@ -1602,10 +1614,10 @@ export interface SharedLinkResponseDto { 'expiresAt': string | null; /** * - * @type {Array} + * @type {Array} * @memberof SharedLinkResponseDto */ - 'assets': Array; + 'assets': Array; /** * * @type {AlbumResponseDto} @@ -1707,6 +1719,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'oauth': SystemConfigOAuthDto; + /** + * + * @type {SystemConfigPasswordLoginDto} + * @memberof SystemConfigDto + */ + 'passwordLogin': SystemConfigPasswordLoginDto; /** * * @type {SystemConfigStorageTemplateDto} @@ -1799,6 +1817,12 @@ export interface SystemConfigOAuthDto { * @memberof SystemConfigOAuthDto */ 'autoRegister': boolean; + /** + * + * @type {boolean} + * @memberof SystemConfigOAuthDto + */ + 'autoLaunch': boolean; /** * * @type {boolean} @@ -1812,6 +1836,19 @@ export interface SystemConfigOAuthDto { */ 'mobileRedirectUri': string; } +/** + * + * @export + * @interface SystemConfigPasswordLoginDto + */ +export interface SystemConfigPasswordLoginDto { + /** + * + * @type {boolean} + * @memberof SystemConfigPasswordLoginDto + */ + 'enabled': boolean; +} /** * * @export diff --git a/web/src/api/utils.ts b/web/src/api/utils.ts index cddada7c86..cb550c31a7 100644 --- a/web/src/api/utils.ts +++ b/web/src/api/utils.ts @@ -22,6 +22,15 @@ export const oauth = { const search = location.search; return search.includes('code=') || search.includes('error='); }, + isAutoLaunchDisabled: (location: Location) => { + const values = ['autoLaunch=0', 'password=1', 'password=true']; + for (const value of values) { + if (location.search.includes(value)) { + return true; + } + } + return false; + }, getConfig: (location: Location) => { const redirectUri = location.href.split('?')[0]; console.log(`OAuth Redirect URI: ${redirectUri}`); diff --git a/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte b/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte new file mode 100644 index 0000000000..c703aa013a --- /dev/null +++ b/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte @@ -0,0 +1,25 @@ + + + + +
+

+ Are you sure you want to disable all login methods? Login will be completely disabled. +

+ +

+ To re-enable, use a + + Server Command. +

+
+
+
diff --git a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte index e2cb1cf9a5..44f860db0e 100644 --- a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte @@ -7,6 +7,7 @@ import { api, SystemConfigOAuthDto } from '@api'; import _ from 'lodash'; import { fade } from 'svelte/transition'; + import ConfirmDisableLogin from '../confirm-disable-login.svelte'; import SettingButtonsRow from '../setting-buttons-row.svelte'; import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte'; import SettingSwitch from '../setting-switch.svelte'; @@ -43,26 +44,43 @@ }); } + let isConfirmOpen = false; + let handleConfirm: (value: boolean) => void; + + const openConfirmModal = () => { + return new Promise((resolve) => { + handleConfirm = (value: boolean) => { + isConfirmOpen = false; + resolve(value); + }; + isConfirmOpen = true; + }); + }; + async function saveSetting() { try { - const { data: currentConfig } = await api.systemConfigApi.getConfig(); + const { data: current } = await api.systemConfigApi.getConfig(); + + if (!current.passwordLogin.enabled && current.oauth.enabled && !oauthConfig.enabled) { + const confirmed = await openConfirmModal(); + if (!confirmed) { + return; + } + } if (!oauthConfig.mobileOverrideEnabled) { oauthConfig.mobileRedirectUri = ''; } - const result = await api.systemConfigApi.updateConfig({ - ...currentConfig, + const { data: updated } = await api.systemConfigApi.updateConfig({ + ...current, oauth: oauthConfig }); - oauthConfig = { ...result.data.oauth }; - savedConfig = { ...result.data.oauth }; + oauthConfig = { ...updated.oauth }; + savedConfig = { ...updated.oauth }; - notificationController.show({ - message: 'OAuth settings saved', - type: NotificationType.Info - }); + notificationController.show({ message: 'OAuth settings saved', type: NotificationType.Info }); } catch (error) { handleError(error, 'Unable to save OAuth settings'); } @@ -80,6 +98,13 @@ } +{#if isConfirmOpen} + handleConfirm(false)} + on:confirm={() => handleConfirm(true)} + /> +{/if} +
{#await getConfigs() then}
@@ -147,6 +172,13 @@ disabled={!oauthConfig.enabled} /> + + + import { + notificationController, + NotificationType + } from '$lib/components/shared-components/notification/notification'; + import { handleError } from '$lib/utils/handle-error'; + import { api, SystemConfigPasswordLoginDto } from '@api'; + import _ from 'lodash'; + import { fade } from 'svelte/transition'; + import ConfirmDisableLogin from '../confirm-disable-login.svelte'; + import SettingButtonsRow from '../setting-buttons-row.svelte'; + import SettingSwitch from '../setting-switch.svelte'; + + export let passwordLoginConfig: SystemConfigPasswordLoginDto; // this is the config that is being edited + + let savedConfig: SystemConfigPasswordLoginDto; + let defaultConfig: SystemConfigPasswordLoginDto; + + async function getConfigs() { + [savedConfig, defaultConfig] = await Promise.all([ + api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin), + api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin) + ]); + } + + let isConfirmOpen = false; + let handleConfirm: (value: boolean) => void; + + const openConfirmModal = () => { + return new Promise((resolve) => { + handleConfirm = (value: boolean) => { + isConfirmOpen = false; + resolve(value); + }; + isConfirmOpen = true; + }); + }; + + async function saveSetting() { + try { + const { data: current } = await api.systemConfigApi.getConfig(); + + if (!current.oauth.enabled && current.passwordLogin.enabled && !passwordLoginConfig.enabled) { + const confirmed = await openConfirmModal(); + if (!confirmed) { + return; + } + } + + const { data: updated } = await api.systemConfigApi.updateConfig({ + ...current, + passwordLogin: passwordLoginConfig + }); + + passwordLoginConfig = { ...updated.passwordLogin }; + savedConfig = { ...updated.passwordLogin }; + + notificationController.show({ message: 'Settings saved', type: NotificationType.Info }); + } catch (error) { + handleError(error, 'Unable to save settings'); + } + } + + async function reset() { + const { data: resetConfig } = await api.systemConfigApi.getConfig(); + + passwordLoginConfig = { ...resetConfig.passwordLogin }; + savedConfig = { ...resetConfig.passwordLogin }; + + notificationController.show({ + message: 'Reset settings to the recent saved settings', + type: NotificationType.Info + }); + } + + async function resetToDefault() { + const { data: configs } = await api.systemConfigApi.getDefaults(); + + passwordLoginConfig = { ...configs.passwordLogin }; + defaultConfig = { ...configs.passwordLogin }; + + notificationController.show({ + message: 'Reset password settings to default', + type: NotificationType.Info + }); + } + + +{#if isConfirmOpen} + handleConfirm(false)} + on:confirm={() => handleConfirm(true)} + /> +{/if} + +
+ {#await getConfigs() then} +
+
+
+
+ + + +
+
+
+
+ {/await} +
diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index 0fea23cc1b..30b8521e4f 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -93,7 +93,6 @@ describe('AlbumCard component', () => { expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith( 'thumbnailIdOne', ThumbnailFormat.Jpeg, - '', { responseType: 'blob' } ); expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailBlob); diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index f2350c66b3..2b78aa6f13 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -439,7 +439,7 @@ const handleDownloadSelectedAssets = async () => { await bulkDownload( - album.albumName, + album.albumName, Array.from(multiSelectAsset), () => { isMultiSelectionMode = false; diff --git a/web/src/lib/components/asset-viewer/album-list-item.svelte b/web/src/lib/components/asset-viewer/album-list-item.svelte index 2e8077644c..1b23a87941 100644 --- a/web/src/lib/components/asset-viewer/album-list-item.svelte +++ b/web/src/lib/components/asset-viewer/album-list-item.svelte @@ -6,7 +6,7 @@ export let album: AlbumResponseDto; export let variant: 'simple' | 'full' = 'full'; - export let searchQuery: string = ''; + export let searchQuery = ''; let albumNameArray: string[] = ['', '', '']; // This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query diff --git a/web/src/lib/components/asset-viewer/video-viewer.svelte b/web/src/lib/components/asset-viewer/video-viewer.svelte index 73ffc0f06b..ba1e580b00 100644 --- a/web/src/lib/components/asset-viewer/video-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-viewer.svelte @@ -9,7 +9,6 @@ export let publicSharedKey = ''; let asset: AssetResponseDto; - let videoPlayerNode: HTMLVideoElement; let isVideoLoading = true; let videoUrl: string; const dispatch = createEventDispatcher(); @@ -55,7 +54,6 @@ class="h-full object-contain" on:canplay={handleCanPlay} on:ended={() => dispatch('onVideoEnded')} - bind:this={videoPlayerNode} > diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte index e1ada7de92..8d7e7e723d 100644 --- a/web/src/lib/components/forms/login-form.svelte +++ b/web/src/lib/components/forms/login-form.svelte @@ -1,4 +1,5 @@ diff --git a/web/src/lib/components/shared-components/portal/portal.svelte b/web/src/lib/components/shared-components/portal/portal.svelte index 0a5bf9dd75..4443896d61 100644 --- a/web/src/lib/components/shared-components/portal/portal.svelte +++ b/web/src/lib/components/shared-components/portal/portal.svelte @@ -47,9 +47,8 @@