mirror of
https://github.com/immich-app/immich.git
synced 2025-01-11 06:10:28 +02:00
feat(server/web): add oauth defaultStorageQuota and storageQuotaClaim (#7548)
* feat(server/web): add oauth defaultStorageQuota and storageQuotaClaim * feat(server/web): fix format and use domain.util constants * address some pr feedback * simplify oauth storage quota logic * adding tests and pr feedback * chore: cleanup --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
8b02f18e99
commit
7303fab9d9
@ -67,9 +67,11 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Client Secret | string | (required) | Required. Client Secret (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) |
|
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label |
|
||||||
|
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage |
|
||||||
|
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| 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 Register | boolean | true | When true, will automatically register a user the first time they sign in |
|
||||||
| Storage Claim | string | preferred_username | Claim mapping for the user's storage label |
|
|
||||||
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
|
| [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 |
|
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |
|
||||||
|
|
||||||
|
@ -95,13 +95,16 @@ The default configuration looks like this:
|
|||||||
"issuerUrl": "",
|
"issuerUrl": "",
|
||||||
"clientId": "",
|
"clientId": "",
|
||||||
"clientSecret": "",
|
"clientSecret": "",
|
||||||
"mobileOverrideEnabled": false,
|
|
||||||
"mobileRedirectUri": "",
|
|
||||||
"scope": "openid email profile",
|
"scope": "openid email profile",
|
||||||
|
"signingAlgorithm": "RS256",
|
||||||
"storageLabelClaim": "preferred_username",
|
"storageLabelClaim": "preferred_username",
|
||||||
|
"storageQuotaClaim": "immich_quota",
|
||||||
|
"defaultStorageQuota": 0,
|
||||||
"buttonText": "Login with OAuth",
|
"buttonText": "Login with OAuth",
|
||||||
"autoRegister": true,
|
"autoRegister": true,
|
||||||
"autoLaunch": false
|
"autoLaunch": false,
|
||||||
|
"mobileOverrideEnabled": false,
|
||||||
|
"mobileRedirectUri": ""
|
||||||
},
|
},
|
||||||
"passwordLogin": {
|
"passwordLogin": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
BIN
mobile/openapi/doc/SystemConfigOAuthDto.md
generated
BIN
mobile/openapi/doc/SystemConfigOAuthDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_o_auth_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_o_auth_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_o_auth_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_o_auth_dto_test.dart
generated
Binary file not shown.
@ -9930,6 +9930,9 @@
|
|||||||
"clientSecret": {
|
"clientSecret": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"defaultStorageQuota": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -9950,6 +9953,9 @@
|
|||||||
},
|
},
|
||||||
"storageLabelClaim": {
|
"storageLabelClaim": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"storageQuotaClaim": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -9958,13 +9964,15 @@
|
|||||||
"buttonText",
|
"buttonText",
|
||||||
"clientId",
|
"clientId",
|
||||||
"clientSecret",
|
"clientSecret",
|
||||||
|
"defaultStorageQuota",
|
||||||
"enabled",
|
"enabled",
|
||||||
"issuerUrl",
|
"issuerUrl",
|
||||||
"mobileOverrideEnabled",
|
"mobileOverrideEnabled",
|
||||||
"mobileRedirectUri",
|
"mobileRedirectUri",
|
||||||
"scope",
|
"scope",
|
||||||
"signingAlgorithm",
|
"signingAlgorithm",
|
||||||
"storageLabelClaim"
|
"storageLabelClaim",
|
||||||
|
"storageQuotaClaim"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
@ -877,6 +877,7 @@ export type SystemConfigOAuthDto = {
|
|||||||
buttonText: string;
|
buttonText: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
|
defaultStorageQuota: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
issuerUrl: string;
|
issuerUrl: string;
|
||||||
mobileOverrideEnabled: boolean;
|
mobileOverrideEnabled: boolean;
|
||||||
@ -884,6 +885,7 @@ export type SystemConfigOAuthDto = {
|
|||||||
scope: string;
|
scope: string;
|
||||||
signingAlgorithm: string;
|
signingAlgorithm: string;
|
||||||
storageLabelClaim: string;
|
storageLabelClaim: string;
|
||||||
|
storageQuotaClaim: string;
|
||||||
};
|
};
|
||||||
export type SystemConfigPasswordLoginDto = {
|
export type SystemConfigPasswordLoginDto = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
newUserTokenRepositoryMock,
|
newUserTokenRepositoryMock,
|
||||||
sharedLinkStub,
|
sharedLinkStub,
|
||||||
systemConfigStub,
|
systemConfigStub,
|
||||||
|
userDto,
|
||||||
userStub,
|
userStub,
|
||||||
userTokenStub,
|
userTokenStub,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
@ -62,14 +63,13 @@ describe('AuthService', () => {
|
|||||||
let userTokenMock: jest.Mocked<IUserTokenRepository>;
|
let userTokenMock: jest.Mocked<IUserTokenRepository>;
|
||||||
let shareMock: jest.Mocked<ISharedLinkRepository>;
|
let shareMock: jest.Mocked<ISharedLinkRepository>;
|
||||||
let keyMock: jest.Mocked<IKeyRepository>;
|
let keyMock: jest.Mocked<IKeyRepository>;
|
||||||
let callbackMock: jest.Mock;
|
|
||||||
|
|
||||||
afterEach(() => {
|
let callbackMock: jest.Mock;
|
||||||
jest.resetModules();
|
let userinfoMock: jest.Mock;
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
callbackMock = jest.fn().mockReturnValue({ access_token: 'access-token' });
|
callbackMock = jest.fn().mockReturnValue({ access_token: 'access-token' });
|
||||||
|
userinfoMock = jest.fn().mockResolvedValue({ sub, email });
|
||||||
|
|
||||||
jest.spyOn(generators, 'state').mockReturnValue('state');
|
jest.spyOn(generators, 'state').mockReturnValue('state');
|
||||||
jest.spyOn(Issuer, 'discover').mockResolvedValue({
|
jest.spyOn(Issuer, 'discover').mockResolvedValue({
|
||||||
@ -83,7 +83,7 @@ describe('AuthService', () => {
|
|||||||
authorizationUrl: jest.fn().mockReturnValue('http://authorization-url'),
|
authorizationUrl: jest.fn().mockReturnValue('http://authorization-url'),
|
||||||
callbackParams: jest.fn().mockReturnValue({ state: 'state' }),
|
callbackParams: jest.fn().mockReturnValue({ state: 'state' }),
|
||||||
callback: callbackMock,
|
callback: callbackMock,
|
||||||
userinfo: jest.fn().mockResolvedValue({ sub, email }),
|
userinfo: userinfoMock,
|
||||||
}),
|
}),
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
@ -491,6 +491,74 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' });
|
expect(callbackMock).toHaveBeenCalledWith('http://mobile-redirect', { state: 'state' }, { state: 'state' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use the default quota', async () => {
|
||||||
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||||
|
userMock.getByEmail.mockResolvedValue(null);
|
||||||
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||||
|
userMock.create.mockResolvedValue(userStub.user1);
|
||||||
|
|
||||||
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
|
loginResponseStub.user1oauth,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(userMock.create).toHaveBeenCalledWith(userDto.userWithDefaultStorageQuota);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore an invalid storage quota', async () => {
|
||||||
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||||
|
userMock.getByEmail.mockResolvedValue(null);
|
||||||
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||||
|
userMock.create.mockResolvedValue(userStub.user1);
|
||||||
|
userinfoMock.mockResolvedValue({ sub, email, immich_quota: 'abc' });
|
||||||
|
|
||||||
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
|
loginResponseStub.user1oauth,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(userMock.create).toHaveBeenCalledWith(userDto.userWithDefaultStorageQuota);
|
||||||
|
});
|
||||||
|
it('should ignore a negative quota', async () => {
|
||||||
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||||
|
userMock.getByEmail.mockResolvedValue(null);
|
||||||
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||||
|
userMock.create.mockResolvedValue(userStub.user1);
|
||||||
|
userinfoMock.mockResolvedValue({ sub, email, immich_quota: -5 });
|
||||||
|
|
||||||
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
|
loginResponseStub.user1oauth,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(userMock.create).toHaveBeenCalledWith(userDto.userWithDefaultStorageQuota);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore a 0 quota', async () => {
|
||||||
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||||
|
userMock.getByEmail.mockResolvedValue(null);
|
||||||
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||||
|
userMock.create.mockResolvedValue(userStub.user1);
|
||||||
|
userinfoMock.mockResolvedValue({ sub, email, immich_quota: 0 });
|
||||||
|
|
||||||
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
|
loginResponseStub.user1oauth,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(userMock.create).toHaveBeenCalledWith(userDto.userWithDefaultStorageQuota);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use a valid storage quota', async () => {
|
||||||
|
configMock.load.mockResolvedValue(systemConfigStub.withDefaultStorageQuota);
|
||||||
|
userMock.getByEmail.mockResolvedValue(null);
|
||||||
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||||
|
userMock.create.mockResolvedValue(userStub.user1);
|
||||||
|
userinfoMock.mockResolvedValue({ sub, email, immich_quota: 5 });
|
||||||
|
|
||||||
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
|
loginResponseStub.user1oauth,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(userMock.create).toHaveBeenCalledWith(userDto.userWithStorageQuotaClaim);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('link', () => {
|
describe('link', () => {
|
||||||
|
@ -7,11 +7,13 @@ import {
|
|||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { isNumber, isString } from 'class-validator';
|
||||||
import cookieParser from 'cookie';
|
import cookieParser from 'cookie';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { IncomingHttpHeaders } from 'node:http';
|
import { IncomingHttpHeaders } from 'node:http';
|
||||||
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
||||||
import { AccessCore, Permission } from '../access';
|
import { AccessCore, Permission } from '../access';
|
||||||
|
import { HumanReadableSize } from '../domain.util';
|
||||||
import {
|
import {
|
||||||
IAccessRepository,
|
IAccessRepository,
|
||||||
ICryptoRepository,
|
ICryptoRepository,
|
||||||
@ -64,6 +66,12 @@ interface OAuthProfile extends UserinfoResponse {
|
|||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClaimOptions<T> {
|
||||||
|
key: string;
|
||||||
|
default: T;
|
||||||
|
isValid: (value: unknown) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
private access: AccessCore;
|
private access: AccessCore;
|
||||||
@ -234,9 +242,11 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = config.oauth;
|
||||||
|
|
||||||
// register new user
|
// register new user
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (!config.oauth.autoRegister) {
|
if (!autoRegister) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Unable to register ${profile.email}. To enable set OAuth Auto Register to true in admin settings.`,
|
`Unable to register ${profile.email}. To enable set OAuth Auto Register to true in admin settings.`,
|
||||||
);
|
);
|
||||||
@ -246,17 +256,24 @@ export class AuthService {
|
|||||||
this.logger.log(`Registering new user: ${profile.email}/${profile.sub}`);
|
this.logger.log(`Registering new user: ${profile.email}/${profile.sub}`);
|
||||||
this.logger.verbose(`OAuth Profile: ${JSON.stringify(profile)}`);
|
this.logger.verbose(`OAuth Profile: ${JSON.stringify(profile)}`);
|
||||||
|
|
||||||
let storageLabel: string | null = profile[config.oauth.storageLabelClaim as keyof OAuthProfile] as string;
|
const storageLabel = this.getClaim(profile, {
|
||||||
if (typeof storageLabel !== 'string') {
|
key: storageLabelClaim,
|
||||||
storageLabel = null;
|
default: '',
|
||||||
}
|
isValid: isString,
|
||||||
|
});
|
||||||
|
const storageQuota = this.getClaim(profile, {
|
||||||
|
key: storageQuotaClaim,
|
||||||
|
default: defaultStorageQuota,
|
||||||
|
isValid: (value: unknown) => isNumber(value) && value > 0,
|
||||||
|
});
|
||||||
|
|
||||||
const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
|
const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
|
||||||
user = await this.userCore.createUser({
|
user = await this.userCore.createUser({
|
||||||
name: userName,
|
name: userName,
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
oauthId: profile.sub,
|
oauthId: profile.sub,
|
||||||
storageLabel,
|
quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null,
|
||||||
|
storageLabel: storageLabel || null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,4 +460,9 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
return [accessTokenCookie, authTypeCookie, isAuthenticatedCookie];
|
return [accessTokenCookie, authTypeCookie, isAuthenticatedCookie];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
||||||
|
const value = profile[options.key as keyof OAuthProfile];
|
||||||
|
return options.isValid(value) ? (value as T) : options.default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IsBoolean, IsNotEmpty, IsString, IsUrl, ValidateIf } from 'class-validator';
|
import { IsBoolean, IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
const isEnabled = (config: SystemConfigOAuthDto) => config.enabled;
|
const isEnabled = (config: SystemConfigOAuthDto) => config.enabled;
|
||||||
const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
|
const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
|
||||||
@ -23,6 +23,10 @@ export class SystemConfigOAuthDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
clientSecret!: string;
|
clientSecret!: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
defaultStorageQuota!: number;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled!: boolean;
|
enabled!: boolean;
|
||||||
|
|
||||||
@ -47,4 +51,7 @@ export class SystemConfigOAuthDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
storageLabelClaim!: string;
|
storageLabelClaim!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
storageQuotaClaim!: string;
|
||||||
}
|
}
|
||||||
|
@ -93,6 +93,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
buttonText: 'Login with OAuth',
|
buttonText: 'Login with OAuth',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
clientSecret: '',
|
clientSecret: '',
|
||||||
|
defaultStorageQuota: 0,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
issuerUrl: '',
|
issuerUrl: '',
|
||||||
mobileOverrideEnabled: false,
|
mobileOverrideEnabled: false,
|
||||||
@ -100,6 +101,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
scope: 'openid email profile',
|
scope: 'openid email profile',
|
||||||
signingAlgorithm: 'RS256',
|
signingAlgorithm: 'RS256',
|
||||||
storageLabelClaim: 'preferred_username',
|
storageLabelClaim: 'preferred_username',
|
||||||
|
storageQuotaClaim: 'immich_quota',
|
||||||
},
|
},
|
||||||
passwordLogin: {
|
passwordLogin: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -93,6 +93,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
buttonText: 'Login with OAuth',
|
buttonText: 'Login with OAuth',
|
||||||
clientId: '',
|
clientId: '',
|
||||||
clientSecret: '',
|
clientSecret: '',
|
||||||
|
defaultStorageQuota: 0,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
issuerUrl: '',
|
issuerUrl: '',
|
||||||
mobileOverrideEnabled: false,
|
mobileOverrideEnabled: false,
|
||||||
@ -100,6 +101,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
scope: 'openid email profile',
|
scope: 'openid email profile',
|
||||||
signingAlgorithm: 'RS256',
|
signingAlgorithm: 'RS256',
|
||||||
storageLabelClaim: 'preferred_username',
|
storageLabelClaim: 'preferred_username',
|
||||||
|
storageQuotaClaim: 'immich_quota',
|
||||||
},
|
},
|
||||||
passwordLogin: {
|
passwordLogin: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -80,6 +80,7 @@ export enum SystemConfigKey {
|
|||||||
OAUTH_BUTTON_TEXT = 'oauth.buttonText',
|
OAUTH_BUTTON_TEXT = 'oauth.buttonText',
|
||||||
OAUTH_CLIENT_ID = 'oauth.clientId',
|
OAUTH_CLIENT_ID = 'oauth.clientId',
|
||||||
OAUTH_CLIENT_SECRET = 'oauth.clientSecret',
|
OAUTH_CLIENT_SECRET = 'oauth.clientSecret',
|
||||||
|
OAUTH_DEFAULT_STORAGE_QUOTA = 'oauth.defaultStorageQuota',
|
||||||
OAUTH_ENABLED = 'oauth.enabled',
|
OAUTH_ENABLED = 'oauth.enabled',
|
||||||
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
|
OAUTH_ISSUER_URL = 'oauth.issuerUrl',
|
||||||
OAUTH_MOBILE_OVERRIDE_ENABLED = 'oauth.mobileOverrideEnabled',
|
OAUTH_MOBILE_OVERRIDE_ENABLED = 'oauth.mobileOverrideEnabled',
|
||||||
@ -87,6 +88,7 @@ export enum SystemConfigKey {
|
|||||||
OAUTH_SCOPE = 'oauth.scope',
|
OAUTH_SCOPE = 'oauth.scope',
|
||||||
OAUTH_SIGNING_ALGORITHM = 'oauth.signingAlgorithm',
|
OAUTH_SIGNING_ALGORITHM = 'oauth.signingAlgorithm',
|
||||||
OAUTH_STORAGE_LABEL_CLAIM = 'oauth.storageLabelClaim',
|
OAUTH_STORAGE_LABEL_CLAIM = 'oauth.storageLabelClaim',
|
||||||
|
OAUTH_STORAGE_QUOTA_CLAIM = 'oauth.storageQuotaClaim',
|
||||||
|
|
||||||
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
|
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
|
||||||
|
|
||||||
@ -227,6 +229,7 @@ export interface SystemConfig {
|
|||||||
buttonText: string;
|
buttonText: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
|
defaultStorageQuota: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
issuerUrl: string;
|
issuerUrl: string;
|
||||||
mobileOverrideEnabled: boolean;
|
mobileOverrideEnabled: boolean;
|
||||||
@ -234,6 +237,7 @@ export interface SystemConfig {
|
|||||||
scope: string;
|
scope: string;
|
||||||
signingAlgorithm: string;
|
signingAlgorithm: string;
|
||||||
storageLabelClaim: string;
|
storageLabelClaim: string;
|
||||||
|
storageQuotaClaim: string;
|
||||||
};
|
};
|
||||||
passwordLogin: {
|
passwordLogin: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
5
server/test/fixtures/system-config.stub.ts
vendored
5
server/test/fixtures/system-config.stub.ts
vendored
@ -22,6 +22,11 @@ export const systemConfigStub: Record<string, SystemConfigEntity[]> = {
|
|||||||
{ key: SystemConfigKey.OAUTH_MOBILE_REDIRECT_URI, value: 'http://mobile-redirect' },
|
{ key: SystemConfigKey.OAUTH_MOBILE_REDIRECT_URI, value: 'http://mobile-redirect' },
|
||||||
{ key: SystemConfigKey.OAUTH_BUTTON_TEXT, value: 'OAuth' },
|
{ key: SystemConfigKey.OAUTH_BUTTON_TEXT, value: 'OAuth' },
|
||||||
],
|
],
|
||||||
|
withDefaultStorageQuota: [
|
||||||
|
{ key: SystemConfigKey.OAUTH_ENABLED, value: true },
|
||||||
|
{ key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: true },
|
||||||
|
{ key: SystemConfigKey.OAUTH_DEFAULT_STORAGE_QUOTA, value: 1 },
|
||||||
|
],
|
||||||
libraryWatchEnabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: true }],
|
libraryWatchEnabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: true }],
|
||||||
libraryWatchDisabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: false }],
|
libraryWatchDisabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: false }],
|
||||||
};
|
};
|
||||||
|
14
server/test/fixtures/user.stub.ts
vendored
14
server/test/fixtures/user.stub.ts
vendored
@ -23,6 +23,20 @@ export const userDto = {
|
|||||||
name: 'User with quota',
|
name: 'User with quota',
|
||||||
quotaSizeInBytes: 42,
|
quotaSizeInBytes: 42,
|
||||||
},
|
},
|
||||||
|
userWithDefaultStorageQuota: {
|
||||||
|
email: 'test@immich.com',
|
||||||
|
name: ' ',
|
||||||
|
oauthId: 'my-auth-user-sub',
|
||||||
|
quotaSizeInBytes: 1_073_741_824,
|
||||||
|
storageLabel: null,
|
||||||
|
},
|
||||||
|
userWithStorageQuotaClaim: {
|
||||||
|
email: 'test@immich.com',
|
||||||
|
name: ' ',
|
||||||
|
oauthId: 'my-auth-user-sub',
|
||||||
|
quotaSizeInBytes: 5_368_709_120,
|
||||||
|
storageLabel: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userStub = {
|
export const userStub = {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
Apply the current
|
Apply the current
|
||||||
<a href={`${AppRoute.ADMIN_SETTINGS}?open=storageTemplate`} class="text-immich-primary dark:text-immich-dark-primary"
|
<a href="{AppRoute.ADMIN_SETTINGS}?open=storageTemplate" class="text-immich-primary dark:text-immich-dark-primary"
|
||||||
>Storage template</a
|
>Storage template</a
|
||||||
>
|
>
|
||||||
to previously uploaded assets
|
to previously uploaded assets
|
||||||
|
@ -130,6 +130,26 @@
|
|||||||
isEdited={!(config.oauth.storageLabelClaim == savedConfig.oauth.storageLabelClaim)}
|
isEdited={!(config.oauth.storageLabelClaim == savedConfig.oauth.storageLabelClaim)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="STORAGE QUOTA CLAIM"
|
||||||
|
desc="Automatically set the user's storage quota to the value of this claim."
|
||||||
|
bind:value={config.oauth.storageQuotaClaim}
|
||||||
|
required={true}
|
||||||
|
disabled={disabled || !config.oauth.enabled}
|
||||||
|
isEdited={!(config.oauth.storageQuotaClaim == savedConfig.oauth.storageQuotaClaim)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
|
label="DEFAULT STORAGE QUOTA (GiB)"
|
||||||
|
desc="Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota)."
|
||||||
|
bind:value={config.oauth.defaultStorageQuota}
|
||||||
|
required={true}
|
||||||
|
disabled={disabled || !config.oauth.enabled}
|
||||||
|
isEdited={!(config.oauth.defaultStorageQuota == savedConfig.oauth.defaultStorageQuota)}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="BUTTON TEXT"
|
label="BUTTON TEXT"
|
||||||
|
Loading…
Reference in New Issue
Block a user