mirror of
https://github.com/immich-app/immich.git
synced 2026-05-31 03:05:22 -04:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00f83e7c66 | |||
| 2da2bef777 | |||
| fd52481582 | |||
| e583e3c55a | |||
| 12e36ad082 | |||
| f4e016edb5 | |||
| d50ea005a1 | |||
| b8c373f0f1 | |||
| b3e5ec48e6 | |||
| 058bd40708 | |||
| 81a885c31d | |||
| 9b7f75a407 | |||
| b42fdcfca9 | |||
| 5731c261eb | |||
| b8591cb591 | |||
| 384d3a0984 | |||
| 03af669856 | |||
| b0e4850d76 | |||
| 36ebcaf00c | |||
| 7a86f2b7b9 | |||
| 55f2b3b6a0 | |||
| fd5e8d6521 | |||
| 6798d5df32 | |||
| 9d33853544 | |||
| a46e46452c | |||
| dbf30b77bf | |||
| 8afca348ff |
@@ -6,6 +6,12 @@ mobile/openapi/**/*.dart linguist-generated=true
|
||||
mobile/lib/**/*.g.dart -diff -merge
|
||||
mobile/lib/**/*.g.dart linguist-generated=true
|
||||
|
||||
mobile/android/**/*.g.kt -diff -merge
|
||||
mobile/android/**/*.g.kt linguist-generated=true
|
||||
|
||||
mobile/ios/**/*.g.swift -diff -merge
|
||||
mobile/ios/**/*.g.swift linguist-generated=true
|
||||
|
||||
mobile/lib/**/*.drift.dart -diff -merge
|
||||
mobile/lib/**/*.drift.dart linguist-generated=true
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
[submodule "mobile/.isar"]
|
||||
path = mobile/.isar
|
||||
url = https://github.com/isar/isar
|
||||
[submodule "e2e/test-assets"]
|
||||
path = e2e/test-assets
|
||||
url = https://github.com/immich-app/test-assets
|
||||
|
||||
@@ -50,6 +50,10 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
||||
- `https://immich.example.com/auth/login`
|
||||
- `https://immich.example.com/user-settings`
|
||||
|
||||
3. Configure Backchannel logout URL
|
||||
|
||||
If the authentication server supports it, the **Backchannel logout URL** can be specified, and it is of the form: `http://DOMAIN:PORT/api/oauth/backchannel-logout`.
|
||||
|
||||
## Enable OAuth
|
||||
|
||||
Once you have a new OAuth client application configured, Immich can be configured using the Administration Settings page, available on the web (Administration -> Settings).
|
||||
@@ -63,6 +67,8 @@ Once you have a new OAuth client application configured, Immich can be configure
|
||||
| `scope` | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||
| `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||
| `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) |
|
||||
| `prompt` | string | (empty) | Prompt parameter for authorization url (examples: select_account, login, consent) |
|
||||
| `end_session_endpoint` | URL | (empty) | Http(s) alternative end session endpoint (logout URI) |
|
||||
| Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up |
|
||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
||||
@@ -181,6 +187,7 @@ Configuration of OAuth in Immich System Settings
|
||||
| Scope | openid email profile immich_scope |
|
||||
| ID Token Signed Response Algorithm | RS256 |
|
||||
| Userinfo Signed Response Algorithm | RS256 |
|
||||
| End Session Endpoint | https://auth.example.com/logout?rd=https://immich.example.com/ |
|
||||
| Storage Label Claim | uid |
|
||||
| Storage Quota Claim | immich_quota |
|
||||
| Default Storage Quota (GiB) | 0 (empty for unlimited quota) |
|
||||
|
||||
@@ -193,6 +193,7 @@ The default configuration looks like this:
|
||||
"defaultStorageQuota": null,
|
||||
"enabled": false,
|
||||
"issuerUrl": "",
|
||||
"endSessionEndpoint": "",
|
||||
"mobileOverrideEnabled": false,
|
||||
"mobileRedirectUri": "",
|
||||
"profileSigningAlgorithm": "none",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { exportJWK, generateKeyPair } from 'jose';
|
||||
import {
|
||||
calculateJwkThumbprint,
|
||||
exportJWK,
|
||||
importPKCS8,
|
||||
importSPKI,
|
||||
SignJWT,
|
||||
} from 'jose';
|
||||
import Provider from 'oidc-provider';
|
||||
import { PRIVATE_KEY_PEM, PUBLIC_KEY_PEM } from './test-keys';
|
||||
|
||||
export enum OAuthClient {
|
||||
DEFAULT = 'client-default',
|
||||
@@ -44,6 +51,29 @@ const claims = [
|
||||
},
|
||||
];
|
||||
|
||||
const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'RS256', {
|
||||
extractable: true,
|
||||
});
|
||||
const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'RS256', {
|
||||
extractable: true,
|
||||
});
|
||||
const kid = await calculateJwkThumbprint(await exportJWK(publicKey));
|
||||
|
||||
export async function generateLogoutToken(iss: string, sub: string) {
|
||||
return await new SignJWT({
|
||||
iss: iss,
|
||||
aud: OAuthClient.DEFAULT,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
jti: crypto.randomUUID(),
|
||||
sub: sub,
|
||||
events: {
|
||||
'http://schemas.openid.net/event/backchannel-logout': {},
|
||||
},
|
||||
})
|
||||
.setProtectedHeader({ alg: 'RS256', typ: 'logout+jwt', kid: kid })
|
||||
.sign(privateKey);
|
||||
}
|
||||
|
||||
const withDefaultClaims = (sub: string) => ({
|
||||
sub,
|
||||
email: `${sub}@immich.app`,
|
||||
@@ -66,10 +96,6 @@ const getClaims = (sub: string, use?: string) => {
|
||||
};
|
||||
|
||||
const setup = async () => {
|
||||
const { privateKey, publicKey } = await generateKeyPair('RS256', {
|
||||
extractable: true,
|
||||
});
|
||||
|
||||
const redirectUris = [
|
||||
'http://127.0.0.1:2285/auth/login',
|
||||
'https://photos.immich.app/oauth/mobile-redirect',
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
export const PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVj5C7hzN3E2HO
|
||||
TcJ+DN/e2NSTQFj4rPylz4J8xjm8Es7l0k2kK5EEGvUNVGZbw7s055c+6kwP9eqg
|
||||
B5XFE7+26Fcq1sou6Tbm310kU4dnMW5l2CgwrhaGyb1pNysao0AMLT60dFYqtUwn
|
||||
ha9ceCsa+ZU1JrknVf3rONtppBvhWoI7CO9XX1keVQ0unHPzCWUjpXTzC8OGEbmB
|
||||
2w7ZIUf8OfJkd5RZ4OtIpML71W9n13aDxT50x2/EW/pFLFtQ/oaleOKHpvlRXDRX
|
||||
W86G4moUJym3gHMXMUj2aOcFG2UJnpLruKz3i5qZwYiTRlBP6O9EIQNCVtYxchuN
|
||||
V1CCcBU1AgMBAAECggEAJLfXMu8Nx89ynPVyyUMMaFfoEpHC9iR0L5obQVpiPMYK
|
||||
VRqVVLecdftPS9s7eQ58BNBRzdC0ZVu841aRYs3HLNbsZZhPkYZQpAxU//Dg5okY
|
||||
fzj7Hv5yidt4HN9+Pd8z/3lRMnj4WapifLaBt8xJ2ujJBMBRxzJBsXDnT0+Kx7+y
|
||||
bYDeuVfyUTEikaK3QZTbuRF3D3eiuN16GG+hv8UqTF2eYbPxdiLjYpTSHa4mH88C
|
||||
qfJz2Xt4SEzmyeo3G+MO17wDFOwtEe8ojlJfULHnHJSFdUwTfYIFM1bg5/fJ9MOS
|
||||
/fO3TSG+wkQqjQa6eoGssAzP87fL2XNLzlDtGY/7uQKBgQDHuJHOtf1EjOvNYiP7
|
||||
EN+8QGs41ghzt9CQRQxWbHpusR3IW3P83KMXwYmrlG70oOUXBRGSB/ESXUofXc5W
|
||||
pu5+Y55S44aUnu/a9yOBttYW0dtHZSL0zFT+PlVASwUzFZ2zcH1KXlUkSpfL5OAD
|
||||
PyDDTnBZ2AWh45fRO9wLo6PPuQKBgQC/tI03RqU3mOjqukKbquYeIpXHfRU5Z0DM
|
||||
u9ru1THYEl6fmkMXycxo/mvW3awyFuyKy/VodqIgKnFgumEqCHZh6OAMm/LC7TfA
|
||||
l9tjFSs/MyOqQVD4kbX+z6Oq4c4GccDoXfsQ3gzECoBapegi/F+6/25y+/C8ghXb
|
||||
J/Jg1GQXXQKBgQDFgWbfzuVZZyrBfu4qGLPJDMN7/114YizknwPma3xf/tN/EcGQ
|
||||
K/k1QvWMMkvPq1UiAKcxjJ0AFjV482FcG9T6NDWbrtmmG88C8Sex3Ue2ZW2+GuwI
|
||||
vhDHJIlV/Vp0/Elp7DJa2xLDwuh+gCZvz3vs6KL+ljxrrhCyn8mp0PfsMQKBgFFZ
|
||||
KnuETOO0zVGdzFoGQTQUdP58A5+iQwsdxB+I9Ge+E80iRso3ZbhADj7VPhbbR3D2
|
||||
b6LuhImluQrUzBpsEOAnU7vGCVPSGdBuIDiBaSKebsn2gYeZPWNtdQQ0YZq2dqek
|
||||
Cb/0mfIuipzsvf7qnSza62F7q4IyqVegMegI+Jg5AoGATM3NMy7JZeKzSkm+3ohU
|
||||
3xZOwgqKV9SH+0OeYWpuBxT7D7FlrKKI4NJ3XN3hg2f/DJAF6dH11CPe7pk94yol
|
||||
HMbh+PQUQ6GYvAzxIOvagWboQ3lzeyubNMpyFjfOrIE/WOQCUBZ9tIwCHIarIuyi
|
||||
QRuNOj3+U8T/n1Ww352HBdw=
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
export const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlY+Qu4czdxNhzk3Cfgzf
|
||||
3tjUk0BY+Kz8pc+CfMY5vBLO5dJNpCuRBBr1DVRmW8O7NOeXPupMD/XqoAeVxRO/
|
||||
tuhXKtbKLuk25t9dJFOHZzFuZdgoMK4Whsm9aTcrGqNADC0+tHRWKrVMJ4WvXHgr
|
||||
GvmVNSa5J1X96zjbaaQb4VqCOwjvV19ZHlUNLpxz8wllI6V08wvDhhG5gdsO2SFH
|
||||
/DnyZHeUWeDrSKTC+9VvZ9d2g8U+dMdvxFv6RSxbUP6GpXjih6b5UVw0V1vOhuJq
|
||||
FCcpt4BzFzFI9mjnBRtlCZ6S67is94uamcGIk0ZQT+jvRCEDQlbWMXIbjVdQgnAV
|
||||
NQIDAQAB
|
||||
-----END PUBLIC KEY-----`;
|
||||
@@ -1,9 +1,10 @@
|
||||
import { OAuthClient, OAuthUser } from '@immich/e2e-auth-server';
|
||||
import { OAuthClient, OAuthUser, generateLogoutToken } from '@immich/e2e-auth-server';
|
||||
import {
|
||||
LoginResponseDto,
|
||||
SystemConfigOAuthDto,
|
||||
getConfigDefaults,
|
||||
getMyUser,
|
||||
getSessions,
|
||||
startOAuth,
|
||||
updateConfig,
|
||||
} from '@immich/sdk';
|
||||
@@ -88,17 +89,19 @@ describe(`/oauth`, () => {
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
|
||||
await setupOAuth(admin.accessToken, {
|
||||
enabled: true,
|
||||
clientId: OAuthClient.DEFAULT,
|
||||
clientSecret: OAuthClient.DEFAULT,
|
||||
buttonText: 'Login with Immich',
|
||||
storageLabelClaim: 'immich_username',
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /oauth/authorize', () => {
|
||||
beforeAll(async () => {
|
||||
await setupOAuth(admin.accessToken, {
|
||||
enabled: true,
|
||||
clientId: OAuthClient.DEFAULT,
|
||||
clientSecret: OAuthClient.DEFAULT,
|
||||
buttonText: 'Login with Immich',
|
||||
storageLabelClaim: 'immich_username',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should throw an error if a redirect uri is not provided`, async () => {
|
||||
const { status, body } = await request(app).post('/oauth/authorize').send({});
|
||||
expect(status).toBe(400);
|
||||
@@ -118,9 +121,46 @@ describe(`/oauth`, () => {
|
||||
expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login');
|
||||
expect(params.get('state')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not include the prompt parameter when not configured', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/oauth/authorize')
|
||||
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
|
||||
expect(status).toBe(201);
|
||||
|
||||
const params = new URL(body.url).searchParams;
|
||||
expect(params.get('prompt')).toBeNull();
|
||||
});
|
||||
|
||||
it('should include the prompt parameter when configured', async () => {
|
||||
await setupOAuth(admin.accessToken, {
|
||||
enabled: true,
|
||||
clientId: OAuthClient.DEFAULT,
|
||||
clientSecret: OAuthClient.DEFAULT,
|
||||
prompt: 'select_account',
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/oauth/authorize')
|
||||
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
|
||||
expect(status).toBe(201);
|
||||
|
||||
const params = new URL(body.url).searchParams;
|
||||
expect(params.get('prompt')).toBe('select_account');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /oauth/callback', () => {
|
||||
beforeAll(async () => {
|
||||
await setupOAuth(admin.accessToken, {
|
||||
enabled: true,
|
||||
clientId: OAuthClient.DEFAULT,
|
||||
clientSecret: OAuthClient.DEFAULT,
|
||||
buttonText: 'Login with Immich',
|
||||
storageLabelClaim: 'immich_username',
|
||||
});
|
||||
});
|
||||
|
||||
it(`should throw an error if a url is not provided`, async () => {
|
||||
const { status, body } = await request(app).post('/oauth/callback').send({});
|
||||
expect(status).toBe(400);
|
||||
@@ -159,37 +199,45 @@ describe(`/oauth`, () => {
|
||||
it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => {
|
||||
const callbackParams = await loginWithOAuth('oauth-auto-register');
|
||||
const { codeVerifier } = await loginWithOAuth('oauth-auto-register');
|
||||
const { status, body } = await request(app)
|
||||
const { status } = await request(app)
|
||||
.post('/oauth/callback')
|
||||
.send({ ...callbackParams, codeVerifier });
|
||||
console.log(body);
|
||||
expect(status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
it('should auto register the user by default', async () => {
|
||||
it('should return a link token for a new OAuth user', async () => {
|
||||
const callbackParams = await loginWithOAuth('oauth-auto-register');
|
||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
accessToken: expect.any(String),
|
||||
isAdmin: false,
|
||||
name: 'OAuth User',
|
||||
userEmail: 'oauth-auto-register@immich.app',
|
||||
userId: expect.any(String),
|
||||
});
|
||||
const response = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.message).toBe('oauth_account_link_required');
|
||||
const setCookie = response.headers['set-cookie'] as unknown as string[];
|
||||
expect(setCookie.some((cookie) => cookie.startsWith('immich_oauth_link_token='))).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow passing state and codeVerifier via cookies', async () => {
|
||||
const { url, state, codeVerifier } = await loginWithOAuth('oauth-auto-register');
|
||||
const { status, body } = await request(app)
|
||||
const response = await request(app)
|
||||
.post('/oauth/callback')
|
||||
.set('Cookie', [`immich_oauth_state=${state}`, `immich_oauth_code_verifier=${codeVerifier}`])
|
||||
.send({ url });
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.message).toBe('oauth_account_link_required');
|
||||
});
|
||||
|
||||
it('should register a new user via POST /auth/register using the link token cookie', async () => {
|
||||
const callbackParams = await loginWithOAuth('oauth-register-flow');
|
||||
const callbackResponse = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(callbackResponse.status).toBe(403);
|
||||
const setCookie = callbackResponse.headers['set-cookie'] as unknown as string[];
|
||||
const linkCookie = setCookie.find((cookie) => cookie.startsWith('immich_oauth_link_token='));
|
||||
expect(linkCookie).toBeDefined();
|
||||
|
||||
const registerResponse = await request(app).post('/auth/register').set('Cookie', linkCookie!);
|
||||
expect(registerResponse.status).toBe(201);
|
||||
expect(registerResponse.body).toMatchObject({
|
||||
accessToken: expect.any(String),
|
||||
userEmail: 'oauth-register-flow@immich.app',
|
||||
userId: expect.any(String),
|
||||
userEmail: 'oauth-auto-register@immich.app',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,26 +358,71 @@ describe(`/oauth`, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should not auto register the user', async () => {
|
||||
it('should still create a link token when auto register is disabled', async () => {
|
||||
const callbackParams = await loginWithOAuth('oauth-no-auto-register');
|
||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
|
||||
const response = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.message).toBe('oauth_account_link_required');
|
||||
});
|
||||
|
||||
it('should link to an existing user by email', async () => {
|
||||
const { userId } = await utils.userSetup(admin.accessToken, {
|
||||
it('should not auto-link to an existing user by email', async () => {
|
||||
await utils.userSetup(admin.accessToken, {
|
||||
name: 'OAuth User 3',
|
||||
email: 'oauth-user3@immich.app',
|
||||
password: 'password',
|
||||
});
|
||||
const callbackParams = await loginWithOAuth('oauth-user3');
|
||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
userId,
|
||||
userEmail: 'oauth-user3@immich.app',
|
||||
});
|
||||
const response = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.message).toBe('oauth_account_link_required');
|
||||
expect(response.body.userEmail).toBe('oauth-user3@immich.app');
|
||||
expect(response.body.oauthLinkToken).toBeUndefined();
|
||||
const setCookie = response.headers['set-cookie'] as unknown as string[];
|
||||
expect(setCookie.some((cookie) => cookie.startsWith('immich_oauth_link_token='))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`POST /oauth/backchannel-logout`, () => {
|
||||
it(`should throw an error if the logout_token is not provided`, async () => {
|
||||
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['[logout_token] Invalid input: expected string, received undefined']));
|
||||
});
|
||||
|
||||
it(`should throw an error if an invalid logout token is provided`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/oauth/backchannel-logout')
|
||||
.send({ logout_token: 'invalid token' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Error backchannel logout: token validation failed'));
|
||||
});
|
||||
|
||||
it(`should logout user if a valid logout token is provided`, async () => {
|
||||
await setupOAuth(admin.accessToken, {
|
||||
enabled: true,
|
||||
clientId: OAuthClient.DEFAULT,
|
||||
clientSecret: OAuthClient.DEFAULT,
|
||||
autoRegister: true,
|
||||
signingAlgorithm: 'RS256',
|
||||
buttonText: 'Login with Immich',
|
||||
});
|
||||
|
||||
const callbackParams = await loginWithOAuth('backchannel-logout-user');
|
||||
const { status: callbackStatus, body: callbackBody } = await request(app)
|
||||
.post('/oauth/callback')
|
||||
.send(callbackParams);
|
||||
expect(callbackStatus).toBe(201);
|
||||
|
||||
await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).resolves.toHaveLength(1);
|
||||
|
||||
const logoutToken = await generateLogoutToken('http://0.0.0.0:2286', 'backchannel-logout-user');
|
||||
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({ logout_token: logoutToken });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({});
|
||||
|
||||
await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).rejects.toMatchObject({
|
||||
status: 401,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -361,24 +454,18 @@ describe(`/oauth`, () => {
|
||||
expect(params.get('state')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should auto register the user by default', async () => {
|
||||
it('should return a link token for a new OAuth user via mobile redirect', async () => {
|
||||
const callbackParams = await loginWithOAuth('oauth-mobile-override', 'app.immich:///oauth-callback');
|
||||
expect(callbackParams.url).toEqual(expect.stringContaining(mobileOverrideRedirectUri));
|
||||
|
||||
// simulate redirecting back to mobile app
|
||||
const url = callbackParams.url.replace(mobileOverrideRedirectUri, 'app.immich:///oauth-callback');
|
||||
|
||||
const { status, body } = await request(app)
|
||||
const response = await request(app)
|
||||
.post('/oauth/callback')
|
||||
.send({ ...callbackParams, url });
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
accessToken: expect.any(String),
|
||||
isAdmin: false,
|
||||
name: 'OAuth User',
|
||||
userEmail: 'oauth-mobile-override@immich.app',
|
||||
userId: expect.any(String),
|
||||
});
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.message).toBe('oauth_account_link_required');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -390,14 +477,9 @@ describe(`/oauth`, () => {
|
||||
clientSecret: OAuthClient.DEFAULT,
|
||||
});
|
||||
const callbackParams = await loginWithOAuth(OAuthUser.ID_TOKEN_CLAIMS);
|
||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
accessToken: expect.any(String),
|
||||
name: 'ID Token User',
|
||||
userEmail: 'oauth-id-token-claims@immich.app',
|
||||
userId: expect.any(String),
|
||||
});
|
||||
const response = await request(app).post('/oauth/callback').send(callbackParams);
|
||||
expect(response.status).toBe(403);
|
||||
expect(response.body.message).toBe('oauth_account_link_required');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -276,9 +276,11 @@
|
||||
"oauth_button_text": "Button text",
|
||||
"oauth_client_secret_description": "Required for confidential client, or if PKCE (Proof Key for Code Exchange) is not supported for public client.",
|
||||
"oauth_enable_description": "Login with OAuth",
|
||||
"oauth_end_session_url_description": "Redirect the user to this URI when they log out.",
|
||||
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
||||
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
||||
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
||||
"oauth_prompt_description": "Prompt parameter (e.g. select_account, login, consent)",
|
||||
"oauth_role_claim": "Role Claim",
|
||||
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
|
||||
"oauth_settings": "OAuth",
|
||||
@@ -851,6 +853,7 @@
|
||||
"create_link_to_share": "Create link to share",
|
||||
"create_link_to_share_description": "Let anyone with the link see the selected photo(s)",
|
||||
"create_new": "CREATE NEW",
|
||||
"create_new_account": "Create new account",
|
||||
"create_new_face": "Create new face",
|
||||
"create_new_person": "Create new person",
|
||||
"create_new_person_hint": "Assign selected assets to a new person",
|
||||
@@ -1123,6 +1126,7 @@
|
||||
"unable_to_hide_person": "Unable to hide person",
|
||||
"unable_to_link_motion_video": "Unable to link motion video",
|
||||
"unable_to_link_oauth_account": "Unable to link OAuth account",
|
||||
"invalid_oauth_relink_token": "This OAuth re-link token is invalid or has expired",
|
||||
"unable_to_log_out_all_devices": "Unable to log out all devices",
|
||||
"unable_to_log_out_device": "Unable to log out device",
|
||||
"unable_to_login_with_oauth": "Unable to login with OAuth",
|
||||
@@ -1640,6 +1644,11 @@
|
||||
"notifications": "Notifications",
|
||||
"notifications_setting_description": "Manage notifications",
|
||||
"oauth": "OAuth",
|
||||
"oauth_account_is_linked": "This account is linked to an OAuth identity. Logging in via OAuth will sign you in directly.",
|
||||
"oauth_account_not_linked": "Link this account to an OAuth identity to sign in via your identity provider.",
|
||||
"oauth_link_existing_account": "Log in with your Immich password to link your OAuth account",
|
||||
"oauth_relink_in_progress": "Redirecting to your identity provider to complete the re-link...",
|
||||
"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_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",
|
||||
|
||||
@@ -15,7 +15,7 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.14.1"
|
||||
flutter = "3.35.7"
|
||||
flutter = "3.41.6"
|
||||
pnpm = "10.33.0"
|
||||
terragrunt = "1.0.0"
|
||||
opentofu = "1.11.5"
|
||||
|
||||
+138
-17
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
@@ -37,36 +37,150 @@ private object BackgroundWorkerPigeonUtils {
|
||||
)
|
||||
}
|
||||
}
|
||||
fun doubleEquals(a: Double, b: Double): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
|
||||
}
|
||||
|
||||
fun floatEquals(a: Float, b: Float): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
|
||||
}
|
||||
|
||||
fun doubleHash(d: Double): Int {
|
||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
||||
val normalized = if (d == 0.0) 0.0 else d
|
||||
val bits = java.lang.Double.doubleToLongBits(normalized)
|
||||
return (bits xor (bits ushr 32)).toInt()
|
||||
}
|
||||
|
||||
fun floatHash(f: Float): Int {
|
||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
||||
val normalized = if (f == 0.0f) 0.0f else f
|
||||
return java.lang.Float.floatToIntBits(normalized)
|
||||
}
|
||||
|
||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
if (a == null || b == null) {
|
||||
return false
|
||||
}
|
||||
if (a is ByteArray && b is ByteArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is IntArray && b is IntArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is LongArray && b is LongArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is DoubleArray && b is DoubleArray) {
|
||||
return a.contentEquals(b)
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!doubleEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is FloatArray && b is FloatArray) {
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!floatEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Array<*> && b is Array<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!deepEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is List<*> && b is List<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
if (a.size != b.size) return false
|
||||
val iterA = a.iterator()
|
||||
val iterB = b.iterator()
|
||||
while (iterA.hasNext() && iterB.hasNext()) {
|
||||
if (!deepEquals(iterA.next(), iterB.next())) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Map<*, *> && b is Map<*, *>) {
|
||||
return a.size == b.size && a.all {
|
||||
(b as Map<Any?, Any?>).containsKey(it.key) &&
|
||||
deepEquals(it.value, b[it.key])
|
||||
if (a.size != b.size) return false
|
||||
for (entry in a) {
|
||||
val key = entry.key
|
||||
var found = false
|
||||
for (bEntry in b) {
|
||||
if (deepEquals(key, bEntry.key)) {
|
||||
if (deepEquals(entry.value, bEntry.value)) {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Double && b is Double) {
|
||||
return doubleEquals(a, b)
|
||||
}
|
||||
if (a is Float && b is Float) {
|
||||
return floatEquals(a, b)
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
|
||||
fun deepHash(value: Any?): Int {
|
||||
return when (value) {
|
||||
null -> 0
|
||||
is ByteArray -> value.contentHashCode()
|
||||
is IntArray -> value.contentHashCode()
|
||||
is LongArray -> value.contentHashCode()
|
||||
is DoubleArray -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + doubleHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is FloatArray -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + floatHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is Array<*> -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + deepHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is List<*> -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + deepHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is Map<*, *> -> {
|
||||
var result = 0
|
||||
for (entry in value) {
|
||||
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
|
||||
}
|
||||
result
|
||||
}
|
||||
is Double -> doubleHash(value)
|
||||
is Float -> floatHash(value)
|
||||
else -> value.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +193,7 @@ class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
) : RuntimeException()
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class BackgroundWorkerSettings (
|
||||
@@ -101,15 +215,22 @@ data class BackgroundWorkerSettings (
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is BackgroundWorkerSettings) {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return BackgroundWorkerPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
val other = other as BackgroundWorkerSettings
|
||||
return BackgroundWorkerPigeonUtils.deepEquals(this.requiresCharging, other.requiresCharging) && BackgroundWorkerPigeonUtils.deepEquals(this.minimumDelaySeconds, other.minimumDelaySeconds)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.requiresCharging)
|
||||
result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.minimumDelaySeconds)
|
||||
return result
|
||||
}
|
||||
}
|
||||
private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
|
||||
Generated
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
@@ -46,7 +46,7 @@ class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
) : RuntimeException()
|
||||
|
||||
enum class NetworkCapability(val raw: Int) {
|
||||
CELLULAR(0),
|
||||
@@ -75,7 +75,7 @@ private open class ConnectivityPigeonCodec : StandardMessageCodec() {
|
||||
when (value) {
|
||||
is NetworkCapability -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw)
|
||||
writeValue(stream, value.raw.toLong())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
|
||||
+150
-20
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
@@ -34,36 +34,150 @@ private object NetworkPigeonUtils {
|
||||
)
|
||||
}
|
||||
}
|
||||
fun doubleEquals(a: Double, b: Double): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
|
||||
}
|
||||
|
||||
fun floatEquals(a: Float, b: Float): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
|
||||
}
|
||||
|
||||
fun doubleHash(d: Double): Int {
|
||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
||||
val normalized = if (d == 0.0) 0.0 else d
|
||||
val bits = java.lang.Double.doubleToLongBits(normalized)
|
||||
return (bits xor (bits ushr 32)).toInt()
|
||||
}
|
||||
|
||||
fun floatHash(f: Float): Int {
|
||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
||||
val normalized = if (f == 0.0f) 0.0f else f
|
||||
return java.lang.Float.floatToIntBits(normalized)
|
||||
}
|
||||
|
||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
if (a == null || b == null) {
|
||||
return false
|
||||
}
|
||||
if (a is ByteArray && b is ByteArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is IntArray && b is IntArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is LongArray && b is LongArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is DoubleArray && b is DoubleArray) {
|
||||
return a.contentEquals(b)
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!doubleEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is FloatArray && b is FloatArray) {
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!floatEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Array<*> && b is Array<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!deepEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is List<*> && b is List<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
if (a.size != b.size) return false
|
||||
val iterA = a.iterator()
|
||||
val iterB = b.iterator()
|
||||
while (iterA.hasNext() && iterB.hasNext()) {
|
||||
if (!deepEquals(iterA.next(), iterB.next())) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Map<*, *> && b is Map<*, *>) {
|
||||
return a.size == b.size && a.all {
|
||||
(b as Map<Any?, Any?>).containsKey(it.key) &&
|
||||
deepEquals(it.value, b[it.key])
|
||||
if (a.size != b.size) return false
|
||||
for (entry in a) {
|
||||
val key = entry.key
|
||||
var found = false
|
||||
for (bEntry in b) {
|
||||
if (deepEquals(key, bEntry.key)) {
|
||||
if (deepEquals(entry.value, bEntry.value)) {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Double && b is Double) {
|
||||
return doubleEquals(a, b)
|
||||
}
|
||||
if (a is Float && b is Float) {
|
||||
return floatEquals(a, b)
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
|
||||
fun deepHash(value: Any?): Int {
|
||||
return when (value) {
|
||||
null -> 0
|
||||
is ByteArray -> value.contentHashCode()
|
||||
is IntArray -> value.contentHashCode()
|
||||
is LongArray -> value.contentHashCode()
|
||||
is DoubleArray -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + doubleHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is FloatArray -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + floatHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is Array<*> -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + deepHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is List<*> -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + deepHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is Map<*, *> -> {
|
||||
var result = 0
|
||||
for (entry in value) {
|
||||
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
|
||||
}
|
||||
result
|
||||
}
|
||||
is Double -> doubleHash(value)
|
||||
is Float -> floatHash(value)
|
||||
else -> value.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +190,7 @@ class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
) : RuntimeException()
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class ClientCertData (
|
||||
@@ -98,15 +212,22 @@ data class ClientCertData (
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ClientCertData) {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return NetworkPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
val other = other as ClientCertData
|
||||
return NetworkPigeonUtils.deepEquals(this.data, other.data) && NetworkPigeonUtils.deepEquals(this.password, other.password)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + NetworkPigeonUtils.deepHash(this.data)
|
||||
result = 31 * result + NetworkPigeonUtils.deepHash(this.password)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
@@ -135,15 +256,24 @@ data class ClientCertPrompt (
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ClientCertPrompt) {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return NetworkPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
val other = other as ClientCertPrompt
|
||||
return NetworkPigeonUtils.deepEquals(this.title, other.title) && NetworkPigeonUtils.deepEquals(this.message, other.message) && NetworkPigeonUtils.deepEquals(this.cancel, other.cancel) && NetworkPigeonUtils.deepEquals(this.confirm, other.confirm)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + NetworkPigeonUtils.deepHash(this.title)
|
||||
result = 31 * result + NetworkPigeonUtils.deepHash(this.message)
|
||||
result = 31 * result + NetworkPigeonUtils.deepHash(this.cancel)
|
||||
result = 31 * result + NetworkPigeonUtils.deepHash(this.confirm)
|
||||
return result
|
||||
}
|
||||
}
|
||||
private open class NetworkPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
@@ -46,7 +46,7 @@ class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
) : RuntimeException()
|
||||
private open class LocalImagesPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return super.readValueOfType(type, buffer)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
||||
+198
-30
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
@@ -34,36 +34,150 @@ private object MessagesPigeonUtils {
|
||||
)
|
||||
}
|
||||
}
|
||||
fun doubleEquals(a: Double, b: Double): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
|
||||
}
|
||||
|
||||
fun floatEquals(a: Float, b: Float): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
|
||||
}
|
||||
|
||||
fun doubleHash(d: Double): Int {
|
||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
||||
val normalized = if (d == 0.0) 0.0 else d
|
||||
val bits = java.lang.Double.doubleToLongBits(normalized)
|
||||
return (bits xor (bits ushr 32)).toInt()
|
||||
}
|
||||
|
||||
fun floatHash(f: Float): Int {
|
||||
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
|
||||
val normalized = if (f == 0.0f) 0.0f else f
|
||||
return java.lang.Float.floatToIntBits(normalized)
|
||||
}
|
||||
|
||||
fun deepEquals(a: Any?, b: Any?): Boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
if (a == null || b == null) {
|
||||
return false
|
||||
}
|
||||
if (a is ByteArray && b is ByteArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is IntArray && b is IntArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is LongArray && b is LongArray) {
|
||||
return a.contentEquals(b)
|
||||
return a.contentEquals(b)
|
||||
}
|
||||
if (a is DoubleArray && b is DoubleArray) {
|
||||
return a.contentEquals(b)
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!doubleEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is FloatArray && b is FloatArray) {
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!floatEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Array<*> && b is Array<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
if (a.size != b.size) return false
|
||||
for (i in a.indices) {
|
||||
if (!deepEquals(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is List<*> && b is List<*>) {
|
||||
return a.size == b.size &&
|
||||
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||
if (a.size != b.size) return false
|
||||
val iterA = a.iterator()
|
||||
val iterB = b.iterator()
|
||||
while (iterA.hasNext() && iterB.hasNext()) {
|
||||
if (!deepEquals(iterA.next(), iterB.next())) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Map<*, *> && b is Map<*, *>) {
|
||||
return a.size == b.size && a.all {
|
||||
(b as Map<Any?, Any?>).containsKey(it.key) &&
|
||||
deepEquals(it.value, b[it.key])
|
||||
if (a.size != b.size) return false
|
||||
for (entry in a) {
|
||||
val key = entry.key
|
||||
var found = false
|
||||
for (bEntry in b) {
|
||||
if (deepEquals(key, bEntry.key)) {
|
||||
if (deepEquals(entry.value, bEntry.value)) {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (a is Double && b is Double) {
|
||||
return doubleEquals(a, b)
|
||||
}
|
||||
if (a is Float && b is Float) {
|
||||
return floatEquals(a, b)
|
||||
}
|
||||
return a == b
|
||||
}
|
||||
|
||||
|
||||
fun deepHash(value: Any?): Int {
|
||||
return when (value) {
|
||||
null -> 0
|
||||
is ByteArray -> value.contentHashCode()
|
||||
is IntArray -> value.contentHashCode()
|
||||
is LongArray -> value.contentHashCode()
|
||||
is DoubleArray -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + doubleHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is FloatArray -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + floatHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is Array<*> -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + deepHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is List<*> -> {
|
||||
var result = 1
|
||||
for (item in value) {
|
||||
result = 31 * result + deepHash(item)
|
||||
}
|
||||
result
|
||||
}
|
||||
is Map<*, *> -> {
|
||||
var result = 0
|
||||
for (entry in value) {
|
||||
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
|
||||
}
|
||||
result
|
||||
}
|
||||
is Double -> doubleHash(value)
|
||||
is Float -> floatHash(value)
|
||||
else -> value.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +190,7 @@ class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
) : RuntimeException()
|
||||
|
||||
enum class PlatformAssetPlaybackStyle(val raw: Int) {
|
||||
UNKNOWN(0),
|
||||
@@ -149,15 +263,34 @@ data class PlatformAsset (
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is PlatformAsset) {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
val other = other as PlatformAsset
|
||||
return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.createdAt, other.createdAt) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.width, other.width) && MessagesPigeonUtils.deepEquals(this.height, other.height) && MessagesPigeonUtils.deepEquals(this.durationInSeconds, other.durationInSeconds) && MessagesPigeonUtils.deepEquals(this.orientation, other.orientation) && MessagesPigeonUtils.deepEquals(this.isFavorite, other.isFavorite) && MessagesPigeonUtils.deepEquals(this.adjustmentTime, other.adjustmentTime) && MessagesPigeonUtils.deepEquals(this.latitude, other.latitude) && MessagesPigeonUtils.deepEquals(this.longitude, other.longitude) && MessagesPigeonUtils.deepEquals(this.playbackStyle, other.playbackStyle)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.id)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.name)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.type)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.createdAt)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.width)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.height)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.durationInSeconds)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.orientation)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.isFavorite)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.adjustmentTime)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.latitude)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.longitude)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.playbackStyle)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
@@ -189,15 +322,25 @@ data class PlatformAlbum (
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is PlatformAlbum) {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
val other = other as PlatformAlbum
|
||||
return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.isCloud, other.isCloud) && MessagesPigeonUtils.deepEquals(this.assetCount, other.assetCount)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.id)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.name)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.isCloud)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetCount)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
@@ -226,15 +369,24 @@ data class SyncDelta (
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is SyncDelta) {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
val other = other as SyncDelta
|
||||
return MessagesPigeonUtils.deepEquals(this.hasChanges, other.hasChanges) && MessagesPigeonUtils.deepEquals(this.updates, other.updates) && MessagesPigeonUtils.deepEquals(this.deletes, other.deletes) && MessagesPigeonUtils.deepEquals(this.assetAlbums, other.assetAlbums)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.hasChanges)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.updates)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.deletes)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetAlbums)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
@@ -260,15 +412,23 @@ data class HashResult (
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is HashResult) {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
val other = other as HashResult
|
||||
return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.hash, other.hash)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.error)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.hash)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
@@ -294,15 +454,23 @@ data class CloudIdResult (
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is CloudIdResult) {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
val other = other as CloudIdResult
|
||||
return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.cloudId, other.cloudId)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.error)
|
||||
result = 31 * result + MessagesPigeonUtils.deepHash(this.cloudId)
|
||||
return result
|
||||
}
|
||||
}
|
||||
private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
@@ -344,7 +512,7 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||
when (value) {
|
||||
is PlatformAssetPlaybackStyle -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw)
|
||||
writeValue(stream, value.raw.toLong())
|
||||
}
|
||||
is PlatformAsset -> {
|
||||
stream.write(130)
|
||||
|
||||
+87
-34
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"\(Swift.type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
@@ -50,6 +50,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||
return value as! T?
|
||||
}
|
||||
|
||||
private func doubleEqualsBackgroundWorker(_ lhs: Double, _ rhs: Double) -> Bool {
|
||||
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
|
||||
}
|
||||
|
||||
private func doubleHashBackgroundWorker(_ value: Double, _ hasher: inout Hasher) {
|
||||
if value.isNaN {
|
||||
hasher.combine(0x7FF8000000000000)
|
||||
} else {
|
||||
// Normalize -0.0 to 0.0
|
||||
hasher.combine(value == 0 ? 0 : value)
|
||||
}
|
||||
}
|
||||
|
||||
func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||
let cleanLhs = nilOrValue(lhs) as Any?
|
||||
let cleanRhs = nilOrValue(rhs) as Any?
|
||||
@@ -60,59 +73,92 @@ func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||
case (nil, _), (_, nil):
|
||||
return false
|
||||
|
||||
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
|
||||
return true
|
||||
|
||||
case is (Void, Void):
|
||||
return true
|
||||
|
||||
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
|
||||
return cleanLhsHashable == cleanRhsHashable
|
||||
|
||||
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
|
||||
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
|
||||
for (index, element) in cleanLhsArray.enumerated() {
|
||||
if !deepEqualsBackgroundWorker(element, cleanRhsArray[index]) {
|
||||
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
|
||||
guard lhsArray.count == rhsArray.count else { return false }
|
||||
for (index, element) in lhsArray.enumerated() {
|
||||
if !deepEqualsBackgroundWorker(element, rhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
|
||||
for (key, cleanLhsValue) in cleanLhsDictionary {
|
||||
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
|
||||
if !deepEqualsBackgroundWorker(cleanLhsValue, cleanRhsDictionary[key]!) {
|
||||
case (let lhsArray, let rhsArray) as ([Double], [Double]):
|
||||
guard lhsArray.count == rhsArray.count else { return false }
|
||||
for (index, element) in lhsArray.enumerated() {
|
||||
if !doubleEqualsBackgroundWorker(element, rhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||
guard lhsDictionary.count == rhsDictionary.count else { return false }
|
||||
for (lhsKey, lhsValue) in lhsDictionary {
|
||||
var found = false
|
||||
for (rhsKey, rhsValue) in rhsDictionary {
|
||||
if deepEqualsBackgroundWorker(lhsKey, rhsKey) {
|
||||
if deepEqualsBackgroundWorker(lhsValue, rhsValue) {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found { return false }
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhs as Double, let rhs as Double):
|
||||
return doubleEqualsBackgroundWorker(lhs, rhs)
|
||||
|
||||
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
|
||||
return lhsHashable == rhsHashable
|
||||
|
||||
default:
|
||||
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func deepHashBackgroundWorker(value: Any?, hasher: inout Hasher) {
|
||||
if let valueList = value as? [AnyHashable] {
|
||||
for item in valueList { deepHashBackgroundWorker(value: item, hasher: &hasher) }
|
||||
return
|
||||
}
|
||||
|
||||
if let valueDict = value as? [AnyHashable: AnyHashable] {
|
||||
for key in valueDict.keys {
|
||||
hasher.combine(key)
|
||||
deepHashBackgroundWorker(value: valueDict[key]!, hasher: &hasher)
|
||||
let cleanValue = nilOrValue(value) as Any?
|
||||
if let cleanValue = cleanValue {
|
||||
if let doubleValue = cleanValue as? Double {
|
||||
doubleHashBackgroundWorker(doubleValue, &hasher)
|
||||
} else if let valueList = cleanValue as? [Any?] {
|
||||
for item in valueList {
|
||||
deepHashBackgroundWorker(value: item, hasher: &hasher)
|
||||
}
|
||||
} else if let valueList = cleanValue as? [Double] {
|
||||
for item in valueList {
|
||||
doubleHashBackgroundWorker(item, &hasher)
|
||||
}
|
||||
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
|
||||
var result = 0
|
||||
for (key, value) in valueDict {
|
||||
var entryKeyHasher = Hasher()
|
||||
deepHashBackgroundWorker(value: key, hasher: &entryKeyHasher)
|
||||
var entryValueHasher = Hasher()
|
||||
deepHashBackgroundWorker(value: value, hasher: &entryValueHasher)
|
||||
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
|
||||
}
|
||||
hasher.combine(result)
|
||||
} else if let hashableValue = cleanValue as? AnyHashable {
|
||||
hasher.combine(hashableValue)
|
||||
} else {
|
||||
hasher.combine(String(describing: cleanValue))
|
||||
}
|
||||
return
|
||||
} else {
|
||||
hasher.combine(0)
|
||||
}
|
||||
|
||||
if let hashableValue = value as? AnyHashable {
|
||||
hasher.combine(hashableValue.hashValue)
|
||||
}
|
||||
|
||||
return hasher.combine(String(describing: value))
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Generated class from Pigeon that represents data sent in messages.
|
||||
struct BackgroundWorkerSettings: Hashable {
|
||||
@@ -137,9 +183,16 @@ struct BackgroundWorkerSettings: Hashable {
|
||||
]
|
||||
}
|
||||
static func == (lhs: BackgroundWorkerSettings, rhs: BackgroundWorkerSettings) -> Bool {
|
||||
return deepEqualsBackgroundWorker(lhs.toList(), rhs.toList()) }
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsBackgroundWorker(lhs.requiresCharging, rhs.requiresCharging) && deepEqualsBackgroundWorker(lhs.minimumDelaySeconds, rhs.minimumDelaySeconds)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashBackgroundWorker(value: toList(), hasher: &hasher)
|
||||
hasher.combine("BackgroundWorkerSettings")
|
||||
deepHashBackgroundWorker(value: requiresCharging, hasher: &hasher)
|
||||
deepHashBackgroundWorker(value: minimumDelaySeconds, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"\(Swift.type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+98
-36
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"\(Swift.type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
@@ -46,6 +46,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||
return value as! T?
|
||||
}
|
||||
|
||||
private func doubleEqualsNetwork(_ lhs: Double, _ rhs: Double) -> Bool {
|
||||
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
|
||||
}
|
||||
|
||||
private func doubleHashNetwork(_ value: Double, _ hasher: inout Hasher) {
|
||||
if value.isNaN {
|
||||
hasher.combine(0x7FF8000000000000)
|
||||
} else {
|
||||
// Normalize -0.0 to 0.0
|
||||
hasher.combine(value == 0 ? 0 : value)
|
||||
}
|
||||
}
|
||||
|
||||
func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||
let cleanLhs = nilOrValue(lhs) as Any?
|
||||
let cleanRhs = nilOrValue(rhs) as Any?
|
||||
@@ -56,59 +69,92 @@ func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||
case (nil, _), (_, nil):
|
||||
return false
|
||||
|
||||
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
|
||||
return true
|
||||
|
||||
case is (Void, Void):
|
||||
return true
|
||||
|
||||
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
|
||||
return cleanLhsHashable == cleanRhsHashable
|
||||
|
||||
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
|
||||
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
|
||||
for (index, element) in cleanLhsArray.enumerated() {
|
||||
if !deepEqualsNetwork(element, cleanRhsArray[index]) {
|
||||
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
|
||||
guard lhsArray.count == rhsArray.count else { return false }
|
||||
for (index, element) in lhsArray.enumerated() {
|
||||
if !deepEqualsNetwork(element, rhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
|
||||
for (key, cleanLhsValue) in cleanLhsDictionary {
|
||||
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
|
||||
if !deepEqualsNetwork(cleanLhsValue, cleanRhsDictionary[key]!) {
|
||||
case (let lhsArray, let rhsArray) as ([Double], [Double]):
|
||||
guard lhsArray.count == rhsArray.count else { return false }
|
||||
for (index, element) in lhsArray.enumerated() {
|
||||
if !doubleEqualsNetwork(element, rhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||
guard lhsDictionary.count == rhsDictionary.count else { return false }
|
||||
for (lhsKey, lhsValue) in lhsDictionary {
|
||||
var found = false
|
||||
for (rhsKey, rhsValue) in rhsDictionary {
|
||||
if deepEqualsNetwork(lhsKey, rhsKey) {
|
||||
if deepEqualsNetwork(lhsValue, rhsValue) {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found { return false }
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhs as Double, let rhs as Double):
|
||||
return doubleEqualsNetwork(lhs, rhs)
|
||||
|
||||
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
|
||||
return lhsHashable == rhsHashable
|
||||
|
||||
default:
|
||||
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func deepHashNetwork(value: Any?, hasher: inout Hasher) {
|
||||
if let valueList = value as? [AnyHashable] {
|
||||
for item in valueList { deepHashNetwork(value: item, hasher: &hasher) }
|
||||
return
|
||||
}
|
||||
|
||||
if let valueDict = value as? [AnyHashable: AnyHashable] {
|
||||
for key in valueDict.keys {
|
||||
hasher.combine(key)
|
||||
deepHashNetwork(value: valueDict[key]!, hasher: &hasher)
|
||||
let cleanValue = nilOrValue(value) as Any?
|
||||
if let cleanValue = cleanValue {
|
||||
if let doubleValue = cleanValue as? Double {
|
||||
doubleHashNetwork(doubleValue, &hasher)
|
||||
} else if let valueList = cleanValue as? [Any?] {
|
||||
for item in valueList {
|
||||
deepHashNetwork(value: item, hasher: &hasher)
|
||||
}
|
||||
} else if let valueList = cleanValue as? [Double] {
|
||||
for item in valueList {
|
||||
doubleHashNetwork(item, &hasher)
|
||||
}
|
||||
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
|
||||
var result = 0
|
||||
for (key, value) in valueDict {
|
||||
var entryKeyHasher = Hasher()
|
||||
deepHashNetwork(value: key, hasher: &entryKeyHasher)
|
||||
var entryValueHasher = Hasher()
|
||||
deepHashNetwork(value: value, hasher: &entryValueHasher)
|
||||
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
|
||||
}
|
||||
hasher.combine(result)
|
||||
} else if let hashableValue = cleanValue as? AnyHashable {
|
||||
hasher.combine(hashableValue)
|
||||
} else {
|
||||
hasher.combine(String(describing: cleanValue))
|
||||
}
|
||||
return
|
||||
} else {
|
||||
hasher.combine(0)
|
||||
}
|
||||
|
||||
if let hashableValue = value as? AnyHashable {
|
||||
hasher.combine(hashableValue.hashValue)
|
||||
}
|
||||
|
||||
return hasher.combine(String(describing: value))
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Generated class from Pigeon that represents data sent in messages.
|
||||
struct ClientCertData: Hashable {
|
||||
@@ -133,9 +179,16 @@ struct ClientCertData: Hashable {
|
||||
]
|
||||
}
|
||||
static func == (lhs: ClientCertData, rhs: ClientCertData) -> Bool {
|
||||
return deepEqualsNetwork(lhs.toList(), rhs.toList()) }
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsNetwork(lhs.data, rhs.data) && deepEqualsNetwork(lhs.password, rhs.password)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashNetwork(value: toList(), hasher: &hasher)
|
||||
hasher.combine("ClientCertData")
|
||||
deepHashNetwork(value: data, hasher: &hasher)
|
||||
deepHashNetwork(value: password, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,9 +223,18 @@ struct ClientCertPrompt: Hashable {
|
||||
]
|
||||
}
|
||||
static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool {
|
||||
return deepEqualsNetwork(lhs.toList(), rhs.toList()) }
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsNetwork(lhs.title, rhs.title) && deepEqualsNetwork(lhs.message, rhs.message) && deepEqualsNetwork(lhs.cancel, rhs.cancel) && deepEqualsNetwork(lhs.confirm, rhs.confirm)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashNetwork(value: toList(), hasher: &hasher)
|
||||
hasher.combine("ClientCertPrompt")
|
||||
deepHashNetwork(value: title, hasher: &hasher)
|
||||
deepHashNetwork(value: message, hasher: &hasher)
|
||||
deepHashNetwork(value: cancel, hasher: &hasher)
|
||||
deepHashNetwork(value: confirm, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"\(Swift.type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"\(Swift.type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+142
-42
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
@@ -50,7 +50,7 @@ private func wrapError(_ error: Any) -> [Any?] {
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"\(Swift.type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
@@ -64,6 +64,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||
return value as! T?
|
||||
}
|
||||
|
||||
private func doubleEqualsMessages(_ lhs: Double, _ rhs: Double) -> Bool {
|
||||
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
|
||||
}
|
||||
|
||||
private func doubleHashMessages(_ value: Double, _ hasher: inout Hasher) {
|
||||
if value.isNaN {
|
||||
hasher.combine(0x7FF8000000000000)
|
||||
} else {
|
||||
// Normalize -0.0 to 0.0
|
||||
hasher.combine(value == 0 ? 0 : value)
|
||||
}
|
||||
}
|
||||
|
||||
func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||
let cleanLhs = nilOrValue(lhs) as Any?
|
||||
let cleanRhs = nilOrValue(rhs) as Any?
|
||||
@@ -74,59 +87,92 @@ func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||
case (nil, _), (_, nil):
|
||||
return false
|
||||
|
||||
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
|
||||
return true
|
||||
|
||||
case is (Void, Void):
|
||||
return true
|
||||
|
||||
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
|
||||
return cleanLhsHashable == cleanRhsHashable
|
||||
|
||||
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
|
||||
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
|
||||
for (index, element) in cleanLhsArray.enumerated() {
|
||||
if !deepEqualsMessages(element, cleanRhsArray[index]) {
|
||||
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
|
||||
guard lhsArray.count == rhsArray.count else { return false }
|
||||
for (index, element) in lhsArray.enumerated() {
|
||||
if !deepEqualsMessages(element, rhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
|
||||
for (key, cleanLhsValue) in cleanLhsDictionary {
|
||||
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
|
||||
if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) {
|
||||
case (let lhsArray, let rhsArray) as ([Double], [Double]):
|
||||
guard lhsArray.count == rhsArray.count else { return false }
|
||||
for (index, element) in lhsArray.enumerated() {
|
||||
if !doubleEqualsMessages(element, rhsArray[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||
guard lhsDictionary.count == rhsDictionary.count else { return false }
|
||||
for (lhsKey, lhsValue) in lhsDictionary {
|
||||
var found = false
|
||||
for (rhsKey, rhsValue) in rhsDictionary {
|
||||
if deepEqualsMessages(lhsKey, rhsKey) {
|
||||
if deepEqualsMessages(lhsValue, rhsValue) {
|
||||
found = true
|
||||
break
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found { return false }
|
||||
}
|
||||
return true
|
||||
|
||||
case (let lhs as Double, let rhs as Double):
|
||||
return doubleEqualsMessages(lhs, rhs)
|
||||
|
||||
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
|
||||
return lhsHashable == rhsHashable
|
||||
|
||||
default:
|
||||
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func deepHashMessages(value: Any?, hasher: inout Hasher) {
|
||||
if let valueList = value as? [AnyHashable] {
|
||||
for item in valueList { deepHashMessages(value: item, hasher: &hasher) }
|
||||
return
|
||||
}
|
||||
|
||||
if let valueDict = value as? [AnyHashable: AnyHashable] {
|
||||
for key in valueDict.keys {
|
||||
hasher.combine(key)
|
||||
deepHashMessages(value: valueDict[key]!, hasher: &hasher)
|
||||
let cleanValue = nilOrValue(value) as Any?
|
||||
if let cleanValue = cleanValue {
|
||||
if let doubleValue = cleanValue as? Double {
|
||||
doubleHashMessages(doubleValue, &hasher)
|
||||
} else if let valueList = cleanValue as? [Any?] {
|
||||
for item in valueList {
|
||||
deepHashMessages(value: item, hasher: &hasher)
|
||||
}
|
||||
} else if let valueList = cleanValue as? [Double] {
|
||||
for item in valueList {
|
||||
doubleHashMessages(item, &hasher)
|
||||
}
|
||||
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
|
||||
var result = 0
|
||||
for (key, value) in valueDict {
|
||||
var entryKeyHasher = Hasher()
|
||||
deepHashMessages(value: key, hasher: &entryKeyHasher)
|
||||
var entryValueHasher = Hasher()
|
||||
deepHashMessages(value: value, hasher: &entryValueHasher)
|
||||
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
|
||||
}
|
||||
hasher.combine(result)
|
||||
} else if let hashableValue = cleanValue as? AnyHashable {
|
||||
hasher.combine(hashableValue)
|
||||
} else {
|
||||
hasher.combine(String(describing: cleanValue))
|
||||
}
|
||||
return
|
||||
} else {
|
||||
hasher.combine(0)
|
||||
}
|
||||
|
||||
if let hashableValue = value as? AnyHashable {
|
||||
hasher.combine(hashableValue.hashValue)
|
||||
}
|
||||
|
||||
return hasher.combine(String(describing: value))
|
||||
}
|
||||
|
||||
|
||||
|
||||
enum PlatformAssetPlaybackStyle: Int {
|
||||
case unknown = 0
|
||||
@@ -208,9 +254,28 @@ struct PlatformAsset: Hashable {
|
||||
]
|
||||
}
|
||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.type, rhs.type) && deepEqualsMessages(lhs.createdAt, rhs.createdAt) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.width, rhs.width) && deepEqualsMessages(lhs.height, rhs.height) && deepEqualsMessages(lhs.durationInSeconds, rhs.durationInSeconds) && deepEqualsMessages(lhs.orientation, rhs.orientation) && deepEqualsMessages(lhs.isFavorite, rhs.isFavorite) && deepEqualsMessages(lhs.adjustmentTime, rhs.adjustmentTime) && deepEqualsMessages(lhs.latitude, rhs.latitude) && deepEqualsMessages(lhs.longitude, rhs.longitude) && deepEqualsMessages(lhs.playbackStyle, rhs.playbackStyle)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashMessages(value: toList(), hasher: &hasher)
|
||||
hasher.combine("PlatformAsset")
|
||||
deepHashMessages(value: id, hasher: &hasher)
|
||||
deepHashMessages(value: name, hasher: &hasher)
|
||||
deepHashMessages(value: type, hasher: &hasher)
|
||||
deepHashMessages(value: createdAt, hasher: &hasher)
|
||||
deepHashMessages(value: updatedAt, hasher: &hasher)
|
||||
deepHashMessages(value: width, hasher: &hasher)
|
||||
deepHashMessages(value: height, hasher: &hasher)
|
||||
deepHashMessages(value: durationInSeconds, hasher: &hasher)
|
||||
deepHashMessages(value: orientation, hasher: &hasher)
|
||||
deepHashMessages(value: isFavorite, hasher: &hasher)
|
||||
deepHashMessages(value: adjustmentTime, hasher: &hasher)
|
||||
deepHashMessages(value: latitude, hasher: &hasher)
|
||||
deepHashMessages(value: longitude, hasher: &hasher)
|
||||
deepHashMessages(value: playbackStyle, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,9 +314,19 @@ struct PlatformAlbum: Hashable {
|
||||
]
|
||||
}
|
||||
static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool {
|
||||
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.isCloud, rhs.isCloud) && deepEqualsMessages(lhs.assetCount, rhs.assetCount)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashMessages(value: toList(), hasher: &hasher)
|
||||
hasher.combine("PlatformAlbum")
|
||||
deepHashMessages(value: id, hasher: &hasher)
|
||||
deepHashMessages(value: name, hasher: &hasher)
|
||||
deepHashMessages(value: updatedAt, hasher: &hasher)
|
||||
deepHashMessages(value: isCloud, hasher: &hasher)
|
||||
deepHashMessages(value: assetCount, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,9 +361,18 @@ struct SyncDelta: Hashable {
|
||||
]
|
||||
}
|
||||
static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool {
|
||||
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsMessages(lhs.hasChanges, rhs.hasChanges) && deepEqualsMessages(lhs.updates, rhs.updates) && deepEqualsMessages(lhs.deletes, rhs.deletes) && deepEqualsMessages(lhs.assetAlbums, rhs.assetAlbums)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashMessages(value: toList(), hasher: &hasher)
|
||||
hasher.combine("SyncDelta")
|
||||
deepHashMessages(value: hasChanges, hasher: &hasher)
|
||||
deepHashMessages(value: updates, hasher: &hasher)
|
||||
deepHashMessages(value: deletes, hasher: &hasher)
|
||||
deepHashMessages(value: assetAlbums, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,9 +403,17 @@ struct HashResult: Hashable {
|
||||
]
|
||||
}
|
||||
static func == (lhs: HashResult, rhs: HashResult) -> Bool {
|
||||
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.hash, rhs.hash)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashMessages(value: toList(), hasher: &hasher)
|
||||
hasher.combine("HashResult")
|
||||
deepHashMessages(value: assetId, hasher: &hasher)
|
||||
deepHashMessages(value: error, hasher: &hasher)
|
||||
deepHashMessages(value: hash, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,9 +444,17 @@ struct CloudIdResult: Hashable {
|
||||
]
|
||||
}
|
||||
static func == (lhs: CloudIdResult, rhs: CloudIdResult) -> Bool {
|
||||
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||
if Swift.type(of: lhs) != Swift.type(of: rhs) {
|
||||
return false
|
||||
}
|
||||
return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.cloudId, rhs.cloudId)
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
deepHashMessages(value: toList(), hasher: &hasher)
|
||||
hasher.combine("CloudIdResult")
|
||||
deepHashMessages(value: assetId, hasher: &hasher)
|
||||
deepHashMessages(value: error, hasher: &hasher)
|
||||
deepHashMessages(value: cloudId, hasher: &hasher)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// ignore_for_file: experimental_member_use
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/partner.provider.dart';
|
||||
|
||||
+111
-115
@@ -1,18 +1,29 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
// ignore_for_file: unused_import, unused_shown_name
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
|
||||
@@ -26,19 +37,65 @@ List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
}
|
||||
if (a is double && b is double) {
|
||||
if (a.isNaN && b.isNaN) {
|
||||
return true;
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
return a.length == b.length &&
|
||||
a.entries.every(
|
||||
(MapEntry<Object?, Object?> entry) =>
|
||||
(b as Map<Object?, Object?>).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]),
|
||||
);
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
for (final MapEntry<Object?, Object?> entryA in a.entries) {
|
||||
bool found = false;
|
||||
for (final MapEntry<Object?, Object?> entryB in b.entries) {
|
||||
if (_deepEquals(entryA.key, entryB.key)) {
|
||||
if (_deepEquals(entryA.value, entryB.value)) {
|
||||
found = true;
|
||||
break;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
int _deepHash(Object? value) {
|
||||
if (value is List) {
|
||||
return Object.hashAll(value.map(_deepHash));
|
||||
}
|
||||
if (value is Map) {
|
||||
int result = 0;
|
||||
for (final MapEntry<Object?, Object?> entry in value.entries) {
|
||||
result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (value is double && value.isNaN) {
|
||||
// Normalize NaN to a consistent hash.
|
||||
return 0x7FF8000000000000.hashCode;
|
||||
}
|
||||
if (value is double && value == 0.0) {
|
||||
// Normalize -0.0 to 0.0 so they have the same hash code.
|
||||
return 0.0.hashCode;
|
||||
}
|
||||
return value.hashCode;
|
||||
}
|
||||
|
||||
class BackgroundWorkerSettings {
|
||||
BackgroundWorkerSettings({required this.requiresCharging, required this.minimumDelaySeconds});
|
||||
|
||||
@@ -68,12 +125,13 @@ class BackgroundWorkerSettings {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
return _deepEquals(requiresCharging, other.requiresCharging) &&
|
||||
_deepEquals(minimumDelaySeconds, other.minimumDelaySeconds);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
@@ -116,95 +174,59 @@ class BackgroundWorkerFgHostApi {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> enable() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<void> saveNotificationMessage(String title, String body) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[title, body]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<void> configure(BackgroundWorkerSettings settings) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[settings]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<void> disable() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,49 +244,31 @@ class BackgroundWorkerBgHostApi {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> onInitialized() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +288,7 @@ abstract class BackgroundWorkerFlutterApi {
|
||||
}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
@@ -293,19 +297,11 @@ abstract class BackgroundWorkerFlutterApi {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
assert(
|
||||
message != null,
|
||||
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload was null.',
|
||||
);
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final bool? arg_isRefresh = (args[0] as bool?);
|
||||
assert(
|
||||
arg_isRefresh != null,
|
||||
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload was null, expected non-null bool.',
|
||||
);
|
||||
final int? arg_maxSeconds = (args[1] as int?);
|
||||
final List<Object?> args = message! as List<Object?>;
|
||||
final bool arg_isRefresh = args[0]! as bool;
|
||||
final int? arg_maxSeconds = args[1] as int?;
|
||||
try {
|
||||
await api.onIosUpload(arg_isRefresh!, arg_maxSeconds);
|
||||
await api.onIosUpload(arg_isRefresh, arg_maxSeconds);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
@@ -318,7 +314,7 @@ abstract class BackgroundWorkerFlutterApi {
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
@@ -341,7 +337,7 @@ abstract class BackgroundWorkerFlutterApi {
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
|
||||
+30
-37
@@ -1,18 +1,29 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
// ignore_for_file: unused_import, unused_shown_name
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
@@ -50,48 +61,30 @@ class BackgroundWorkerLockApi {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> lock() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<void> unlock() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
}
|
||||
|
||||
+31
-29
@@ -1,18 +1,29 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
// ignore_for_file: unused_import, unused_shown_name
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
enum NetworkCapability { cellular, wifi, vpn, unmetered }
|
||||
@@ -36,7 +47,7 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
final value = readValue(buffer) as int?;
|
||||
return value == null ? null : NetworkCapability.values[value];
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
@@ -58,30 +69,21 @@ class ConnectivityApi {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<List<NetworkCapability>> getCapabilities() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<NetworkCapability>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as List<Object?>).cast<NetworkCapability>();
|
||||
}
|
||||
}
|
||||
|
||||
+45
-56
@@ -1,18 +1,29 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
// ignore_for_file: unused_import, unused_shown_name
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
@@ -57,9 +68,9 @@ class LocalImageApi {
|
||||
required bool isVideo,
|
||||
required bool preferEncoded,
|
||||
}) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
@@ -72,68 +83,46 @@ class LocalImageApi {
|
||||
isVideo,
|
||||
preferEncoded,
|
||||
]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)?.cast<String, int>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
return (pigeonVar_replyValue as Map<Object?, Object?>?)?.cast<String, int>();
|
||||
}
|
||||
|
||||
Future<void> cancelRequest(int requestId) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[requestId]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<Map<String, int>> getThumbhash(String thumbhash) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[thumbhash]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)!.cast<String, int>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as Map<Object?, Object?>).cast<String, int>();
|
||||
}
|
||||
}
|
||||
|
||||
+212
-241
@@ -1,34 +1,91 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
// ignore_for_file: unused_import, unused_shown_name
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
}
|
||||
if (a is double && b is double) {
|
||||
if (a.isNaN && b.isNaN) {
|
||||
return true;
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
return a.length == b.length &&
|
||||
a.entries.every(
|
||||
(MapEntry<Object?, Object?> entry) =>
|
||||
(b as Map<Object?, Object?>).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]),
|
||||
);
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
for (final MapEntry<Object?, Object?> entryA in a.entries) {
|
||||
bool found = false;
|
||||
for (final MapEntry<Object?, Object?> entryB in b.entries) {
|
||||
if (_deepEquals(entryA.key, entryB.key)) {
|
||||
if (_deepEquals(entryA.value, entryB.value)) {
|
||||
found = true;
|
||||
break;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
int _deepHash(Object? value) {
|
||||
if (value is List) {
|
||||
return Object.hashAll(value.map(_deepHash));
|
||||
}
|
||||
if (value is Map) {
|
||||
int result = 0;
|
||||
for (final MapEntry<Object?, Object?> entry in value.entries) {
|
||||
result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (value is double && value.isNaN) {
|
||||
// Normalize NaN to a consistent hash.
|
||||
return 0x7FF8000000000000.hashCode;
|
||||
}
|
||||
if (value is double && value == 0.0) {
|
||||
// Normalize -0.0 to 0.0 so they have the same hash code.
|
||||
return 0.0.hashCode;
|
||||
}
|
||||
return value.hashCode;
|
||||
}
|
||||
|
||||
enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping }
|
||||
|
||||
class PlatformAsset {
|
||||
@@ -129,12 +186,25 @@ class PlatformAsset {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
return _deepEquals(id, other.id) &&
|
||||
_deepEquals(name, other.name) &&
|
||||
_deepEquals(type, other.type) &&
|
||||
_deepEquals(createdAt, other.createdAt) &&
|
||||
_deepEquals(updatedAt, other.updatedAt) &&
|
||||
_deepEquals(width, other.width) &&
|
||||
_deepEquals(height, other.height) &&
|
||||
_deepEquals(durationInSeconds, other.durationInSeconds) &&
|
||||
_deepEquals(orientation, other.orientation) &&
|
||||
_deepEquals(isFavorite, other.isFavorite) &&
|
||||
_deepEquals(adjustmentTime, other.adjustmentTime) &&
|
||||
_deepEquals(latitude, other.latitude) &&
|
||||
_deepEquals(longitude, other.longitude) &&
|
||||
_deepEquals(playbackStyle, other.playbackStyle);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class PlatformAlbum {
|
||||
@@ -184,12 +254,16 @@ class PlatformAlbum {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
return _deepEquals(id, other.id) &&
|
||||
_deepEquals(name, other.name) &&
|
||||
_deepEquals(updatedAt, other.updatedAt) &&
|
||||
_deepEquals(isCloud, other.isCloud) &&
|
||||
_deepEquals(assetCount, other.assetCount);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class SyncDelta {
|
||||
@@ -215,9 +289,9 @@ class SyncDelta {
|
||||
result as List<Object?>;
|
||||
return SyncDelta(
|
||||
hasChanges: result[0]! as bool,
|
||||
updates: (result[1] as List<Object?>?)!.cast<PlatformAsset>(),
|
||||
deletes: (result[2] as List<Object?>?)!.cast<String>(),
|
||||
assetAlbums: (result[3] as Map<Object?, Object?>?)!.cast<String, List<String>>(),
|
||||
updates: (result[1]! as List<Object?>).cast<PlatformAsset>(),
|
||||
deletes: (result[2]! as List<Object?>).cast<String>(),
|
||||
assetAlbums: (result[3]! as Map<Object?, Object?>).cast<String, List<String>>(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -230,12 +304,15 @@ class SyncDelta {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
return _deepEquals(hasChanges, other.hasChanges) &&
|
||||
_deepEquals(updates, other.updates) &&
|
||||
_deepEquals(deletes, other.deletes) &&
|
||||
_deepEquals(assetAlbums, other.assetAlbums);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class HashResult {
|
||||
@@ -269,12 +346,12 @@ class HashResult {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
return _deepEquals(assetId, other.assetId) && _deepEquals(error, other.error) && _deepEquals(hash, other.hash);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class CloudIdResult {
|
||||
@@ -308,12 +385,14 @@ class CloudIdResult {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
return _deepEquals(assetId, other.assetId) &&
|
||||
_deepEquals(error, other.error) &&
|
||||
_deepEquals(cloudId, other.cloudId);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
@@ -350,7 +429,7 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
final value = readValue(buffer) as int?;
|
||||
return value == null ? null : PlatformAssetPlaybackStyle.values[value];
|
||||
case 130:
|
||||
return PlatformAsset.decode(readValue(buffer)!);
|
||||
@@ -382,323 +461,215 @@ class NativeSyncApi {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<bool> shouldFullSync() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as bool?)!;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return pigeonVar_replyValue! as bool;
|
||||
}
|
||||
|
||||
Future<SyncDelta> getMediaChanges() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as SyncDelta?)!;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return pigeonVar_replyValue! as SyncDelta;
|
||||
}
|
||||
|
||||
Future<void> checkpointSync() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<void> clearSyncCheckpoint() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<List<String>> getAssetIdsForAlbum(String albumId) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[albumId]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<String>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as List<Object?>).cast<String>();
|
||||
}
|
||||
|
||||
Future<List<PlatformAlbum>> getAlbums() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAlbum>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as List<Object?>).cast<PlatformAlbum>();
|
||||
}
|
||||
|
||||
Future<int> getAssetsCountSince(String albumId, int timestamp) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[albumId, timestamp]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as int?)!;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return pigeonVar_replyValue! as int;
|
||||
}
|
||||
|
||||
Future<List<PlatformAsset>> getAssetsForAlbum(String albumId, {int? updatedTimeCond}) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[albumId, updatedTimeCond]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAsset>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as List<Object?>).cast<PlatformAsset>();
|
||||
}
|
||||
|
||||
Future<List<HashResult>> hashAssets(List<String> assetIds, {bool allowNetworkAccess = false}) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[assetIds, allowNetworkAccess]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<HashResult>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as List<Object?>).cast<HashResult>();
|
||||
}
|
||||
|
||||
Future<void> cancelHashing() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<Map<String, List<PlatformAsset>>> getTrashedAssets() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)!.cast<String, List<PlatformAsset>>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as Map<Object?, Object?>).cast<String, List<PlatformAsset>>();
|
||||
}
|
||||
|
||||
Future<List<CloudIdResult>> getCloudIdForAssetIds(List<String> assetIds) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[assetIds]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<CloudIdResult>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as List<Object?>).cast<CloudIdResult>();
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+118
-112
@@ -1,34 +1,91 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
// ignore_for_file: unused_import, unused_shown_name
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
}
|
||||
if (a is double && b is double) {
|
||||
if (a.isNaN && b.isNaN) {
|
||||
return true;
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
return a.length == b.length &&
|
||||
a.entries.every(
|
||||
(MapEntry<Object?, Object?> entry) =>
|
||||
(b as Map<Object?, Object?>).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]),
|
||||
);
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
for (final MapEntry<Object?, Object?> entryA in a.entries) {
|
||||
bool found = false;
|
||||
for (final MapEntry<Object?, Object?> entryB in b.entries) {
|
||||
if (_deepEquals(entryA.key, entryB.key)) {
|
||||
if (_deepEquals(entryA.value, entryB.value)) {
|
||||
found = true;
|
||||
break;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
int _deepHash(Object? value) {
|
||||
if (value is List) {
|
||||
return Object.hashAll(value.map(_deepHash));
|
||||
}
|
||||
if (value is Map) {
|
||||
int result = 0;
|
||||
for (final MapEntry<Object?, Object?> entry in value.entries) {
|
||||
result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (value is double && value.isNaN) {
|
||||
// Normalize NaN to a consistent hash.
|
||||
return 0x7FF8000000000000.hashCode;
|
||||
}
|
||||
if (value is double && value == 0.0) {
|
||||
// Normalize -0.0 to 0.0 so they have the same hash code.
|
||||
return 0.0.hashCode;
|
||||
}
|
||||
return value.hashCode;
|
||||
}
|
||||
|
||||
class ClientCertData {
|
||||
ClientCertData({required this.data, required this.password});
|
||||
|
||||
@@ -58,12 +115,12 @@ class ClientCertData {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
return _deepEquals(data, other.data) && _deepEquals(password, other.password);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class ClientCertPrompt {
|
||||
@@ -104,12 +161,15 @@ class ClientCertPrompt {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
return _deepEquals(title, other.title) &&
|
||||
_deepEquals(message, other.message) &&
|
||||
_deepEquals(cancel, other.cancel) &&
|
||||
_deepEquals(confirm, other.confirm);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
@@ -157,150 +217,96 @@ class NetworkApi {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> addCertificate(ClientCertData clientData) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[clientData]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<void> selectCertificate(ClientCertPrompt promptText) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[promptText]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<void> removeCertificate() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<bool> hasCertificate() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as bool?)!;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return pigeonVar_replyValue! as bool;
|
||||
}
|
||||
|
||||
Future<int> getClientPointer() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as int?)!;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return pigeonVar_replyValue! as int;
|
||||
}
|
||||
|
||||
Future<void> setRequestHeaders(Map<String, String> headers, List<String> serverUrls, String? token) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[headers, serverUrls, token]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
}
|
||||
|
||||
+45
-56
@@ -1,18 +1,29 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
// ignore_for_file: unused_import, unused_shown_name
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
@@ -50,76 +61,54 @@ class RemoteImageApi {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<Map<String, int>?> requestImage(String url, {required int requestId, required bool preferEncoded}) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[url, requestId, preferEncoded]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)?.cast<String, int>();
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
return (pigeonVar_replyValue as Map<Object?, Object?>?)?.cast<String, int>();
|
||||
}
|
||||
|
||||
Future<void> cancelRequest(int requestId) async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[requestId]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
|
||||
}
|
||||
|
||||
Future<int> clearCache() async {
|
||||
final String pigeonVar_channelName =
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as int?)!;
|
||||
}
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return pigeonVar_replyValue! as int;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget {
|
||||
final isOwner = album.ownerId == userId;
|
||||
|
||||
void showErrorMessage() {
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "shared_album_section_people_action_error".t(context: context),
|
||||
@@ -60,7 +60,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget {
|
||||
showErrorMessage();
|
||||
}
|
||||
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
}
|
||||
|
||||
Future<void> addUsers() async {
|
||||
|
||||
@@ -33,7 +33,7 @@ class DriftMapPage extends StatelessWidget {
|
||||
top: 70,
|
||||
child: IconButton.filled(
|
||||
color: Colors.white,
|
||||
onPressed: () => context.pop(),
|
||||
onPressed: () => ContextHelper(context).pop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
style: IconButton.styleFrom(
|
||||
padding: const EdgeInsets.all(8),
|
||||
|
||||
@@ -58,11 +58,11 @@ class _DriftPersonPageState extends ConsumerState<DriftPersonPage> {
|
||||
return PersonOptionSheet(
|
||||
onEditName: () async {
|
||||
await handleEditName(context);
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
},
|
||||
onEditBirthday: () async {
|
||||
await handleEditBirthday(context);
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
},
|
||||
birthdayExists: _person.birthDate != null,
|
||||
);
|
||||
|
||||
@@ -340,11 +340,11 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
child: QuickDatePicker(
|
||||
currentInput: dateInputFilter.value,
|
||||
onRequestPicker: () {
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
showDatePicker();
|
||||
},
|
||||
onSelect: (date) {
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
datePicked(date);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -833,7 +833,7 @@ class CreateAlbumButton extends ConsumerWidget {
|
||||
// Invalidate using the asset's remote ID to refresh the "Appears in" list
|
||||
ref.invalidate(albumsContainingAssetProvider(asset.remoteId!));
|
||||
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
}
|
||||
|
||||
return SliverPadding(
|
||||
|
||||
+2
-2
@@ -6,10 +6,10 @@ import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||
import 'package:immich_mobile/utils/people.utils.dart';
|
||||
@@ -73,7 +73,7 @@ class PeopleDetails extends ConsumerWidget {
|
||||
context.back();
|
||||
return;
|
||||
}
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
context.pushRoute(DriftPersonRoute(person: person));
|
||||
},
|
||||
onNameTap: () => showNameEditModal(person),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||
|
||||
@@ -45,6 +45,16 @@ class AppLogDetailRouteArgs {
|
||||
String toString() {
|
||||
return 'AppLogDetailRouteArgs{key: $key, logMessage: $logMessage}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! AppLogDetailRouteArgs) return false;
|
||||
return key == other.key && logMessage == other.logMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ logMessage.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -98,6 +108,16 @@ class AssetTroubleshootRouteArgs {
|
||||
String toString() {
|
||||
return 'AssetTroubleshootRouteArgs{key: $key, asset: $asset}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! AssetTroubleshootRouteArgs) return false;
|
||||
return key == other.key && asset == other.asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ asset.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -162,6 +182,25 @@ class AssetViewerRouteArgs {
|
||||
String toString() {
|
||||
return 'AssetViewerRouteArgs{key: $key, initialIndex: $initialIndex, timelineService: $timelineService, heroOffset: $heroOffset, currentAlbum: $currentAlbum}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! AssetViewerRouteArgs) return false;
|
||||
return key == other.key &&
|
||||
initialIndex == other.initialIndex &&
|
||||
timelineService == other.timelineService &&
|
||||
heroOffset == other.heroOffset &&
|
||||
currentAlbum == other.currentAlbum;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
key.hashCode ^
|
||||
initialIndex.hashCode ^
|
||||
timelineService.hashCode ^
|
||||
heroOffset.hashCode ^
|
||||
currentAlbum.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -215,6 +254,18 @@ class CleanupPreviewRouteArgs {
|
||||
String toString() {
|
||||
return 'CleanupPreviewRouteArgs{key: $key, assets: $assets}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! CleanupPreviewRouteArgs) return false;
|
||||
return key == other.key &&
|
||||
const ListEquality<LocalAsset>().equals(assets, other.assets);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
key.hashCode ^ const ListEquality<LocalAsset>().hash(assets);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -289,6 +340,20 @@ class DriftActivitiesRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftActivitiesRouteArgs{key: $key, album: $album, assetId: $assetId, assetName: $assetName}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftActivitiesRouteArgs) return false;
|
||||
return key == other.key &&
|
||||
album == other.album &&
|
||||
assetId == other.assetId &&
|
||||
assetName == other.assetName;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
key.hashCode ^ album.hashCode ^ assetId.hashCode ^ assetName.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -326,6 +391,16 @@ class DriftAlbumOptionsRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftAlbumOptionsRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftAlbumOptionsRouteArgs) return false;
|
||||
return key == other.key && album == other.album;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ album.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -407,6 +482,21 @@ class DriftAssetSelectionTimelineRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftAssetSelectionTimelineRouteArgs{key: $key, lockedSelectionAssets: $lockedSelectionAssets}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftAssetSelectionTimelineRouteArgs) return false;
|
||||
return key == other.key &&
|
||||
const SetEquality<BaseAsset>().equals(
|
||||
lockedSelectionAssets,
|
||||
other.lockedSelectionAssets,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
key.hashCode ^ const SetEquality<BaseAsset>().hash(lockedSelectionAssets);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -539,6 +629,16 @@ class DriftEditImageRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftEditImageRouteArgs{key: $key, image: $image, applyEdits: $applyEdits}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftEditImageRouteArgs) return false;
|
||||
return key == other.key && image == other.image;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ image.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -642,6 +742,16 @@ class DriftMapRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftMapRouteArgs{key: $key, initialLocation: $initialLocation}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftMapRouteArgs) return false;
|
||||
return key == other.key && initialLocation == other.initialLocation;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ initialLocation.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -694,6 +804,21 @@ class DriftMemoryRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftMemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftMemoryRouteArgs) return false;
|
||||
return const ListEquality<DriftMemory>().equals(memories, other.memories) &&
|
||||
memoryIndex == other.memoryIndex &&
|
||||
key == other.key;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
const ListEquality<DriftMemory>().hash(memories) ^
|
||||
memoryIndex.hashCode ^
|
||||
key.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -732,6 +857,16 @@ class DriftPartnerDetailRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftPartnerDetailRouteArgs{key: $key, partner: $partner}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftPartnerDetailRouteArgs) return false;
|
||||
return key == other.key && partner == other.partner;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ partner.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -801,6 +936,16 @@ class DriftPersonRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftPersonRouteArgs{key: $key, person: $person}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftPersonRouteArgs) return false;
|
||||
return key == other.key && person == other.person;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ person.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -838,6 +983,16 @@ class DriftPlaceDetailRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftPlaceDetailRouteArgs{key: $key, place: $place}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftPlaceDetailRouteArgs) return false;
|
||||
return key == other.key && place == other.place;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ place.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -880,6 +1035,16 @@ class DriftPlaceRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftPlaceRouteArgs{key: $key, currentLocation: $currentLocation}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftPlaceRouteArgs) return false;
|
||||
return key == other.key && currentLocation == other.currentLocation;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ currentLocation.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -982,6 +1147,16 @@ class DriftUserSelectionRouteArgs {
|
||||
String toString() {
|
||||
return 'DriftUserSelectionRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! DriftUserSelectionRouteArgs) return false;
|
||||
return key == other.key && album == other.album;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ album.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1037,6 +1212,16 @@ class FolderRouteArgs {
|
||||
String toString() {
|
||||
return 'FolderRouteArgs{key: $key, folder: $folder}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! FolderRouteArgs) return false;
|
||||
return key == other.key && folder == other.folder;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ folder.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1106,6 +1291,16 @@ class LocalTimelineRouteArgs {
|
||||
String toString() {
|
||||
return 'LocalTimelineRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! LocalTimelineRouteArgs) return false;
|
||||
return key == other.key && album == other.album;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ album.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1186,6 +1381,16 @@ class MapLocationPickerRouteArgs {
|
||||
String toString() {
|
||||
return 'MapLocationPickerRouteArgs{key: $key, initialLatLng: $initialLatLng}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! MapLocationPickerRouteArgs) return false;
|
||||
return key == other.key && initialLatLng == other.initialLatLng;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ initialLatLng.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1225,6 +1430,16 @@ class PinAuthRouteArgs {
|
||||
String toString() {
|
||||
return 'PinAuthRouteArgs{key: $key, createPinCode: $createPinCode}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! PinAuthRouteArgs) return false;
|
||||
return key == other.key && createPinCode == other.createPinCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ createPinCode.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1263,6 +1478,16 @@ class ProfilePictureCropRouteArgs {
|
||||
String toString() {
|
||||
return 'ProfilePictureCropRouteArgs{key: $key, asset: $asset}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! ProfilePictureCropRouteArgs) return false;
|
||||
return key == other.key && asset == other.asset;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ asset.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1300,6 +1525,16 @@ class RemoteAlbumRouteArgs {
|
||||
String toString() {
|
||||
return 'RemoteAlbumRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! RemoteAlbumRouteArgs) return false;
|
||||
return key == other.key && album == other.album;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => key.hashCode ^ album.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1369,6 +1604,16 @@ class SettingsSubRouteArgs {
|
||||
String toString() {
|
||||
return 'SettingsSubRouteArgs{section: $section, key: $key}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! SettingsSubRouteArgs) return false;
|
||||
return section == other.section && key == other.key;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => section.hashCode ^ key.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1406,6 +1651,22 @@ class ShareIntentRouteArgs {
|
||||
String toString() {
|
||||
return 'ShareIntentRouteArgs{key: $key, attachments: $attachments}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! ShareIntentRouteArgs) return false;
|
||||
return key == other.key &&
|
||||
const ListEquality<ShareIntentAttachment>().equals(
|
||||
attachments,
|
||||
other.attachments,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
key.hashCode ^
|
||||
const ListEquality<ShareIntentAttachment>().hash(attachments);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
@@ -1466,6 +1727,23 @@ class SharedLinkEditRouteArgs {
|
||||
String toString() {
|
||||
return 'SharedLinkEditRouteArgs{key: $key, existingLink: $existingLink, assetsList: $assetsList, albumId: $albumId}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! SharedLinkEditRouteArgs) return false;
|
||||
return key == other.key &&
|
||||
existingLink == other.existingLink &&
|
||||
const ListEquality<String>().equals(assetsList, other.assetsList) &&
|
||||
albumId == other.albumId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
key.hashCode ^
|
||||
existingLink.hashCode ^
|
||||
const ListEquality<String>().hash(assetsList) ^
|
||||
albumId.hashCode;
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
|
||||
@@ -50,7 +50,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => context.pop(),
|
||||
onPressed: () => ContextHelper(context).pop(),
|
||||
icon: Icon(Icons.close, size: 20, color: context.colorScheme.onSurfaceVariant),
|
||||
),
|
||||
Align(
|
||||
@@ -179,7 +179,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication);
|
||||
},
|
||||
child: Text("documentation", style: context.textTheme.bodySmall).tr(),
|
||||
@@ -187,7 +187,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
const SizedBox(width: 20, child: Text("•", textAlign: TextAlign.center)),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication);
|
||||
},
|
||||
child: Text("profile_drawer_github", style: context.textTheme.bodySmall).tr(),
|
||||
@@ -195,7 +195,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
const SizedBox(width: 20, child: Text("•", textAlign: TextAlign.center)),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
context.pop();
|
||||
ContextHelper(context).pop();
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
showLicensePage(
|
||||
context: context,
|
||||
@@ -235,7 +235,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
return Dismissible(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
direction: DismissDirection.down,
|
||||
onDismissed: (_) => context.pop(),
|
||||
onDismissed: (_) => ContextHelper(context).pop(),
|
||||
key: const Key('app_bar_dialog'),
|
||||
child: Dialog(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
|
||||
@@ -107,7 +107,7 @@ class _LocationPicker extends HookWidget {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
onPressed: () => ContextHelper(context).pop(),
|
||||
child: Text(
|
||||
"cancel",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
|
||||
@@ -703,11 +703,11 @@ class _DeleteConfirmationDialog extends StatelessWidget {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(false),
|
||||
onPressed: () => ContextHelper(context).pop(false),
|
||||
child: Text('cancel'.t(context: context)),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.pop(true),
|
||||
onPressed: () => ContextHelper(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: context.colorScheme.error,
|
||||
foregroundColor: context.colorScheme.onError,
|
||||
@@ -747,7 +747,7 @@ class _DeleteSuccessDialog extends StatelessWidget {
|
||||
),
|
||||
actions: [
|
||||
ElevatedButton(
|
||||
onPressed: () => context.pop(),
|
||||
onPressed: () => ContextHelper(context).pop(),
|
||||
child: Text('done'.t(context: context)),
|
||||
),
|
||||
],
|
||||
|
||||
Generated
+1
-1
@@ -122,10 +122,10 @@ Class | Method | HTTP request | Description
|
||||
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code | Change pin code
|
||||
*AuthenticationApi* | [**finishOAuth**](doc//AuthenticationApi.md#finishoauth) | **POST** /oauth/callback | Finish OAuth
|
||||
*AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status | Retrieve auth status
|
||||
*AuthenticationApi* | [**linkOAuthAccount**](doc//AuthenticationApi.md#linkoauthaccount) | **POST** /oauth/link | Link OAuth account
|
||||
*AuthenticationApi* | [**lockAuthSession**](doc//AuthenticationApi.md#lockauthsession) | **POST** /auth/session/lock | Lock auth session
|
||||
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | Login
|
||||
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | Logout
|
||||
*AuthenticationApi* | [**logoutOAuth**](doc//AuthenticationApi.md#logoutoauth) | **POST** /oauth/backchannel-logout | Backchannel OAuth logout
|
||||
*AuthenticationApi* | [**redirectOAuthToMobile**](doc//AuthenticationApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | Redirect OAuth to mobile
|
||||
*AuthenticationApi* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code | Reset pin code
|
||||
*AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /auth/pin-code | Setup pin code
|
||||
|
||||
+53
-56
@@ -224,62 +224,6 @@ class AuthenticationApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Link OAuth account
|
||||
///
|
||||
/// Link an OAuth account to the authenticated user.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [OAuthCallbackDto] oAuthCallbackDto (required):
|
||||
Future<Response> linkOAuthAccountWithHttpInfo(OAuthCallbackDto oAuthCallbackDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/oauth/link';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = oAuthCallbackDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Link OAuth account
|
||||
///
|
||||
/// Link an OAuth account to the authenticated user.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [OAuthCallbackDto] oAuthCallbackDto (required):
|
||||
Future<UserAdminResponseDto?> linkOAuthAccount(OAuthCallbackDto oAuthCallbackDto,) async {
|
||||
final response = await linkOAuthAccountWithHttpInfo(oAuthCallbackDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Lock auth session
|
||||
///
|
||||
/// Remove elevated access to locked assets from the current session.
|
||||
@@ -424,6 +368,59 @@ class AuthenticationApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Backchannel OAuth logout
|
||||
///
|
||||
/// Logout the OAuth account and invalidate the session specified by the sid claim or all sessions if the sid claim is not present.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] logoutToken (required):
|
||||
/// OAuth logout token
|
||||
Future<Response> logoutOAuthWithHttpInfo(String logoutToken,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/oauth/backchannel-logout';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/x-www-form-urlencoded'];
|
||||
|
||||
if (logoutToken != null) {
|
||||
formParams[r'logout_token'] = parameterToString(logoutToken);
|
||||
}
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Backchannel OAuth logout
|
||||
///
|
||||
/// Logout the OAuth account and invalidate the session specified by the sid claim or all sessions if the sid claim is not present.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] logoutToken (required):
|
||||
/// OAuth logout token
|
||||
Future<void> logoutOAuth(String logoutToken,) async {
|
||||
final response = await logoutOAuthWithHttpInfo(logoutToken,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Redirect OAuth to mobile
|
||||
///
|
||||
/// Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting.
|
||||
|
||||
+19
-1
@@ -21,10 +21,12 @@ class SystemConfigOAuthDto {
|
||||
required this.clientSecret,
|
||||
required this.defaultStorageQuota,
|
||||
required this.enabled,
|
||||
required this.endSessionEndpoint,
|
||||
required this.issuerUrl,
|
||||
required this.mobileOverrideEnabled,
|
||||
required this.mobileRedirectUri,
|
||||
required this.profileSigningAlgorithm,
|
||||
required this.prompt,
|
||||
required this.roleClaim,
|
||||
required this.scope,
|
||||
required this.signingAlgorithm,
|
||||
@@ -60,6 +62,9 @@ class SystemConfigOAuthDto {
|
||||
/// Enabled
|
||||
bool enabled;
|
||||
|
||||
/// End session endpoint
|
||||
String endSessionEndpoint;
|
||||
|
||||
/// Issuer URL
|
||||
String issuerUrl;
|
||||
|
||||
@@ -72,6 +77,9 @@ class SystemConfigOAuthDto {
|
||||
/// Profile signing algorithm
|
||||
String profileSigningAlgorithm;
|
||||
|
||||
/// OAuth prompt parameter (e.g. select_account, login, consent)
|
||||
String prompt;
|
||||
|
||||
/// Role claim
|
||||
String roleClaim;
|
||||
|
||||
@@ -105,10 +113,12 @@ class SystemConfigOAuthDto {
|
||||
other.clientSecret == clientSecret &&
|
||||
other.defaultStorageQuota == defaultStorageQuota &&
|
||||
other.enabled == enabled &&
|
||||
other.endSessionEndpoint == endSessionEndpoint &&
|
||||
other.issuerUrl == issuerUrl &&
|
||||
other.mobileOverrideEnabled == mobileOverrideEnabled &&
|
||||
other.mobileRedirectUri == mobileRedirectUri &&
|
||||
other.profileSigningAlgorithm == profileSigningAlgorithm &&
|
||||
other.prompt == prompt &&
|
||||
other.roleClaim == roleClaim &&
|
||||
other.scope == scope &&
|
||||
other.signingAlgorithm == signingAlgorithm &&
|
||||
@@ -128,10 +138,12 @@ class SystemConfigOAuthDto {
|
||||
(clientSecret.hashCode) +
|
||||
(defaultStorageQuota == null ? 0 : defaultStorageQuota!.hashCode) +
|
||||
(enabled.hashCode) +
|
||||
(endSessionEndpoint.hashCode) +
|
||||
(issuerUrl.hashCode) +
|
||||
(mobileOverrideEnabled.hashCode) +
|
||||
(mobileRedirectUri.hashCode) +
|
||||
(profileSigningAlgorithm.hashCode) +
|
||||
(prompt.hashCode) +
|
||||
(roleClaim.hashCode) +
|
||||
(scope.hashCode) +
|
||||
(signingAlgorithm.hashCode) +
|
||||
@@ -141,7 +153,7 @@ class SystemConfigOAuthDto {
|
||||
(tokenEndpointAuthMethod.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigOAuthDto[allowInsecureRequests=$allowInsecureRequests, autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
|
||||
String toString() => 'SystemConfigOAuthDto[allowInsecureRequests=$allowInsecureRequests, autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, endSessionEndpoint=$endSessionEndpoint, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, prompt=$prompt, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -157,10 +169,12 @@ class SystemConfigOAuthDto {
|
||||
// json[r'defaultStorageQuota'] = null;
|
||||
}
|
||||
json[r'enabled'] = this.enabled;
|
||||
json[r'endSessionEndpoint'] = this.endSessionEndpoint;
|
||||
json[r'issuerUrl'] = this.issuerUrl;
|
||||
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
|
||||
json[r'mobileRedirectUri'] = this.mobileRedirectUri;
|
||||
json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm;
|
||||
json[r'prompt'] = this.prompt;
|
||||
json[r'roleClaim'] = this.roleClaim;
|
||||
json[r'scope'] = this.scope;
|
||||
json[r'signingAlgorithm'] = this.signingAlgorithm;
|
||||
@@ -190,10 +204,12 @@ class SystemConfigOAuthDto {
|
||||
? null
|
||||
: num.parse('${json[r'defaultStorageQuota']}'),
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
endSessionEndpoint: mapValueOfType<String>(json, r'endSessionEndpoint')!,
|
||||
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
|
||||
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
|
||||
mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!,
|
||||
profileSigningAlgorithm: mapValueOfType<String>(json, r'profileSigningAlgorithm')!,
|
||||
prompt: mapValueOfType<String>(json, r'prompt')!,
|
||||
roleClaim: mapValueOfType<String>(json, r'roleClaim')!,
|
||||
scope: mapValueOfType<String>(json, r'scope')!,
|
||||
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
|
||||
@@ -256,10 +272,12 @@ class SystemConfigOAuthDto {
|
||||
'clientSecret',
|
||||
'defaultStorageQuota',
|
||||
'enabled',
|
||||
'endSessionEndpoint',
|
||||
'issuerUrl',
|
||||
'mobileOverrideEnabled',
|
||||
'mobileRedirectUri',
|
||||
'profileSigningAlgorithm',
|
||||
'prompt',
|
||||
'roleClaim',
|
||||
'scope',
|
||||
'signingAlgorithm',
|
||||
|
||||
+99
-68
@@ -5,18 +5,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
|
||||
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "80.0.0"
|
||||
version: "93.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
|
||||
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
version: "10.0.1"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -53,18 +53,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_route
|
||||
sha256: "1d1bd908a1fec327719326d5d0791edd37f16caff6493c01003689fb03315ad7"
|
||||
sha256: e9acfeb3df33d188fce4ad0239ef4238f333b7aa4d95ec52af3c2b9360dcd969
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0+1"
|
||||
version: "11.1.0"
|
||||
auto_route_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: auto_route_generator
|
||||
sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46
|
||||
sha256: "7aa0e90874928e78709f0a21a69fb5bc2ae1aa932dec862930d2af85c40adb01"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.1"
|
||||
version: "10.5.0"
|
||||
background_downloader:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -133,18 +133,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
|
||||
sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "4.0.5"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_config
|
||||
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
|
||||
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.3.0"
|
||||
build_daemon:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -153,30 +153,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||
sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.15"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
version: "2.13.1"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -189,10 +173,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
|
||||
sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.5"
|
||||
version: "8.12.5"
|
||||
cast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -241,14 +225,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
|
||||
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.10.1"
|
||||
version: "4.11.1"
|
||||
collection:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -326,10 +318,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
|
||||
sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.7"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -365,28 +357,27 @@ packages:
|
||||
drift:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: drift
|
||||
ref: "53ef7e9f19fe8f68416251760b4b99fe43f1c575"
|
||||
resolved-ref: "53ef7e9f19fe8f68416251760b4b99fe43f1c575"
|
||||
url: "https://github.com/immich-app/drift"
|
||||
source: git
|
||||
version: "2.26.0"
|
||||
name: drift
|
||||
sha256: "055c249d1f91be5a47fe447f88afc24c4ca6f4cd6c5ed66767b4797d48acc2e5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.32.1"
|
||||
drift_dev:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: drift_dev
|
||||
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
|
||||
sha256: "88a9de3af8571518148a6d8a513b57779fd1e60a026d3ab8a481a878fba01d91"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.26.0"
|
||||
version: "2.32.1"
|
||||
drift_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: drift_flutter
|
||||
sha256: b52bd710f809db11e25259d429d799d034ba1c5224ce6a73fe8419feb980d44c
|
||||
sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.6"
|
||||
version: "0.3.0"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -785,6 +776,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.1"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
hooks_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -793,6 +792,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
hotreloader:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hotreloader
|
||||
sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -981,6 +988,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
lean_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lean_builder
|
||||
sha256: ee4117b03e93a4eb83e1a78c8e7a1dc22188d43bb142309982be48673a1b3a53
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.7"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1101,6 +1116,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
native_video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1322,10 +1345,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pigeon
|
||||
sha256: "0045b172d1da43c40cb3f58e80e04b50a65cba20b8b70dc880af04181f7758da"
|
||||
sha256: "04cfefc8add8b47ddf9ccac8b92bb4edeb67c87f185c623ba0db118ac99334ad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "26.0.2"
|
||||
version: "26.3.4"
|
||||
pinput:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1600,18 +1623,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||
sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "4.2.2"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.10.2"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1660,30 +1683,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlcipher_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlcipher_flutter_libs
|
||||
sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0+eol"
|
||||
sqlite3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.5"
|
||||
version: "3.3.1"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
|
||||
sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.31"
|
||||
version: "0.6.0+eol"
|
||||
sqlparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlparser
|
||||
sha256: "27dd0a9f0c02e22ac0eb42a23df9ea079ce69b52bb4a3b478d64e0ef34a263ee"
|
||||
sha256: ab2b467425f1d4f3acfa5fd11a08226f7d6c26ff102c06be1807e1dff34e050b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.41.0"
|
||||
version: "0.44.3"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1772,14 +1803,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timing
|
||||
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1936,10 +1959,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2020,6 +2043,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
xxh3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xxh3
|
||||
sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+5
-12
@@ -10,7 +10,7 @@ environment:
|
||||
|
||||
dependencies:
|
||||
async: ^2.13.0
|
||||
auto_route: ^9.2.0
|
||||
auto_route: ^11.1.0
|
||||
background_downloader: ^9.3.0
|
||||
cast: ^2.1.0
|
||||
collection: ^1.19.1
|
||||
@@ -19,8 +19,8 @@ dependencies:
|
||||
crypto: ^3.0.6
|
||||
device_info_plus: ^12.2.0
|
||||
# DB
|
||||
drift: ^2.26.0
|
||||
drift_flutter: ^0.2.6
|
||||
drift: ^2.32.1
|
||||
drift_flutter: ^0.3.0
|
||||
dynamic_color: ^1.8.1
|
||||
easy_localization: ^3.0.8
|
||||
ffi: ^2.1.4
|
||||
@@ -91,10 +91,10 @@ dependencies:
|
||||
path: pkgs/ok_http/
|
||||
|
||||
dev_dependencies:
|
||||
auto_route_generator: ^9.0.0
|
||||
auto_route_generator: ^10.5.0
|
||||
build_runner: ^2.4.8
|
||||
# Drift generator
|
||||
drift_dev: ^2.26.0
|
||||
drift_dev: ^2.32.1
|
||||
fake_async: ^1.3.3
|
||||
file: ^7.0.1 # for MemoryFileSystem
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
@@ -108,13 +108,6 @@ dev_dependencies:
|
||||
# Type safe platform code
|
||||
pigeon: ^26.0.2
|
||||
|
||||
dependency_overrides:
|
||||
drift:
|
||||
git:
|
||||
url: https://github.com/immich-app/drift
|
||||
ref: '53ef7e9f19fe8f68416251760b4b99fe43f1c575'
|
||||
path: drift/
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
|
||||
@@ -1285,6 +1285,59 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/admin/users/{id}/oauth-relink-token": {
|
||||
"post": {
|
||||
"description": "Create a single-use token that lets a user re-link their account to a new OAuth sub (e.g. when migrating IdPs). Deliver the token to the user out-of-band.",
|
||||
"operationId": "createOAuthReLinkTokenAdmin",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthReLinkTokenResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Issue an OAuth re-link token",
|
||||
"tags": [
|
||||
"Users (admin)"
|
||||
],
|
||||
"x-immich-admin-only": true,
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Added"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "adminUser.update"
|
||||
}
|
||||
},
|
||||
"/admin/users/{id}/preferences": {
|
||||
"get": {
|
||||
"description": "Retrieve the preferences of a specific user.",
|
||||
@@ -4660,6 +4713,35 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"description": "Create a new user from a pending OAuth link token (requires OAuth auto-register to be enabled).",
|
||||
"operationId": "register",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LoginResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Register via OAuth",
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Added"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/auth/session/lock": {
|
||||
"post": {
|
||||
"description": "Remove elevated access to locked assets from the current session.",
|
||||
@@ -7359,6 +7441,38 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/oauth/backchannel-logout": {
|
||||
"post": {
|
||||
"description": "Logout the OAuth account and invalidate the session specified by the sid claim or all sessions if the sid claim is not present.",
|
||||
"operationId": "logoutOAuth",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthBackchannelLogoutDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Backchannel OAuth logout",
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Added"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/oauth/callback": {
|
||||
"post": {
|
||||
"description": "Complete the OAuth authorization process by exchanging the authorization code for a session token.",
|
||||
@@ -7407,65 +7521,6 @@
|
||||
"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/OAuthCallbackDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"get": {
|
||||
"description": "Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting.",
|
||||
@@ -7497,6 +7552,38 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/oauth/relink-start": {
|
||||
"post": {
|
||||
"description": "Redeem an admin-issued OAuth re-link token, setting a short-lived cookie that gets consumed by the subsequent OAuth callback.",
|
||||
"operationId": "startOAuthReLink",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthReLinkStartDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Start OAuth re-link",
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Added"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/oauth/unlink": {
|
||||
"post": {
|
||||
"description": "Unlink the OAuth account from the authenticated user.",
|
||||
@@ -19031,6 +19118,18 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"OAuthBackchannelLogoutDto": {
|
||||
"properties": {
|
||||
"logout_token": {
|
||||
"description": "OAuth logout token",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"logout_token"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"OAuthCallbackDto": {
|
||||
"properties": {
|
||||
"codeVerifier": {
|
||||
@@ -19072,6 +19171,38 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"OAuthReLinkStartDto": {
|
||||
"properties": {
|
||||
"token": {
|
||||
"description": "Plaintext OAuth re-link token issued by an administrator",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"token"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"OAuthReLinkTokenResponseDto": {
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"description": "Token expiration",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "Single-use token; deliver to the user via /auth/link?token=<token>",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"expiresAt",
|
||||
"token"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"OAuthTokenEndpointAuthMethod": {
|
||||
"description": "OAuth token endpoint auth method",
|
||||
"enum": [
|
||||
@@ -21145,6 +21276,10 @@
|
||||
"description": "Whether OAuth auto-launch is enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"oauthAutoRegister": {
|
||||
"description": "Whether OAuth auto-register is enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ocr": {
|
||||
"description": "Whether OCR is enabled",
|
||||
"type": "boolean"
|
||||
@@ -21183,6 +21318,7 @@
|
||||
"map",
|
||||
"oauth",
|
||||
"oauthAutoLaunch",
|
||||
"oauthAutoRegister",
|
||||
"ocr",
|
||||
"passwordLogin",
|
||||
"reverseGeocoding",
|
||||
@@ -24287,6 +24423,10 @@
|
||||
"description": "Enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"endSessionEndpoint": {
|
||||
"description": "End session endpoint",
|
||||
"type": "string"
|
||||
},
|
||||
"issuerUrl": {
|
||||
"description": "Issuer URL",
|
||||
"type": "string"
|
||||
@@ -24303,6 +24443,10 @@
|
||||
"description": "Profile signing algorithm",
|
||||
"type": "string"
|
||||
},
|
||||
"prompt": {
|
||||
"description": "OAuth prompt parameter (e.g. select_account, login, consent)",
|
||||
"type": "string"
|
||||
},
|
||||
"roleClaim": {
|
||||
"description": "Role claim",
|
||||
"type": "string"
|
||||
@@ -24342,10 +24486,12 @@
|
||||
"clientSecret",
|
||||
"defaultStorageQuota",
|
||||
"enabled",
|
||||
"endSessionEndpoint",
|
||||
"issuerUrl",
|
||||
"mobileOverrideEnabled",
|
||||
"mobileRedirectUri",
|
||||
"profileSigningAlgorithm",
|
||||
"prompt",
|
||||
"roleClaim",
|
||||
"scope",
|
||||
"signingAlgorithm",
|
||||
|
||||
@@ -262,6 +262,12 @@ export type UserAdminUpdateDto = {
|
||||
/** Storage label */
|
||||
storageLabel?: string | null;
|
||||
};
|
||||
export type OAuthReLinkTokenResponseDto = {
|
||||
/** Token expiration */
|
||||
expiresAt: string;
|
||||
/** Single-use token; deliver to the user via /auth/link?token=<token> */
|
||||
token: string;
|
||||
};
|
||||
export type AlbumsResponse = {
|
||||
defaultAssetOrder: AssetOrder;
|
||||
};
|
||||
@@ -1409,6 +1415,10 @@ export type OAuthAuthorizeResponseDto = {
|
||||
/** OAuth authorization URL */
|
||||
url: string;
|
||||
};
|
||||
export type OAuthBackchannelLogoutDto = {
|
||||
/** OAuth logout token */
|
||||
logout_token: string;
|
||||
};
|
||||
export type OAuthCallbackDto = {
|
||||
/** OAuth code verifier (PKCE) */
|
||||
codeVerifier?: string;
|
||||
@@ -1417,6 +1427,10 @@ export type OAuthCallbackDto = {
|
||||
/** OAuth callback URL */
|
||||
url: string;
|
||||
};
|
||||
export type OAuthReLinkStartDto = {
|
||||
/** Plaintext OAuth re-link token issued by an administrator */
|
||||
token: string;
|
||||
};
|
||||
export type PartnerResponseDto = {
|
||||
avatarColor: UserAvatarColor;
|
||||
/** User email */
|
||||
@@ -2043,6 +2057,8 @@ export type ServerFeaturesDto = {
|
||||
oauth: boolean;
|
||||
/** Whether OAuth auto-launch is enabled */
|
||||
oauthAutoLaunch: boolean;
|
||||
/** Whether OAuth auto-register is enabled */
|
||||
oauthAutoRegister: boolean;
|
||||
/** Whether OCR is enabled */
|
||||
ocr: boolean;
|
||||
/** Whether password login is enabled */
|
||||
@@ -2514,6 +2530,8 @@ export type SystemConfigOAuthDto = {
|
||||
defaultStorageQuota: number | null;
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
/** End session endpoint */
|
||||
endSessionEndpoint: string;
|
||||
/** Issuer URL */
|
||||
issuerUrl: string;
|
||||
/** Mobile override enabled */
|
||||
@@ -2522,6 +2540,8 @@ export type SystemConfigOAuthDto = {
|
||||
mobileRedirectUri: string;
|
||||
/** Profile signing algorithm */
|
||||
profileSigningAlgorithm: string;
|
||||
/** OAuth prompt parameter (e.g. select_account, login, consent) */
|
||||
prompt: string;
|
||||
/** Role claim */
|
||||
roleClaim: string;
|
||||
/** Scope */
|
||||
@@ -3517,6 +3537,20 @@ export function updateUserAdmin({ id, userAdminUpdateDto }: {
|
||||
body: userAdminUpdateDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Issue an OAuth re-link token
|
||||
*/
|
||||
export function createOAuthReLinkTokenAdmin({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: OAuthReLinkTokenResponseDto;
|
||||
}>(`/admin/users/${encodeURIComponent(id)}/oauth-relink-token`, {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Retrieve user preferences
|
||||
*/
|
||||
@@ -4296,6 +4330,18 @@ export function changePinCode({ pinCodeChangeDto }: {
|
||||
body: pinCodeChangeDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Register via OAuth
|
||||
*/
|
||||
export function register(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: LoginResponseDto;
|
||||
}>("/auth/register", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Lock auth session
|
||||
*/
|
||||
@@ -4909,6 +4955,18 @@ export function startOAuth({ oAuthConfigDto }: {
|
||||
body: oAuthConfigDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Backchannel OAuth logout
|
||||
*/
|
||||
export function logoutOAuth({ oAuthBackchannelLogoutDto }: {
|
||||
oAuthBackchannelLogoutDto: OAuthBackchannelLogoutDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/oauth/backchannel-logout", oazapfts.form({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: oAuthBackchannelLogoutDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Finish OAuth
|
||||
*/
|
||||
@@ -4924,21 +4982,6 @@ export function finishOAuth({ oAuthCallbackDto }: {
|
||||
body: oAuthCallbackDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Link OAuth account
|
||||
*/
|
||||
export function linkOAuthAccount({ oAuthCallbackDto }: {
|
||||
oAuthCallbackDto: OAuthCallbackDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: UserAdminResponseDto;
|
||||
}>("/oauth/link", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: oAuthCallbackDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Redirect OAuth to mobile
|
||||
*/
|
||||
@@ -4947,6 +4990,18 @@ export function redirectOAuthToMobile(opts?: Oazapfts.RequestOpts) {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Start OAuth re-link
|
||||
*/
|
||||
export function startOAuthReLink({ oAuthReLinkStartDto }: {
|
||||
oAuthReLinkStartDto: OAuthReLinkStartDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/oauth/relink-start", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: oAuthReLinkStartDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Unlink OAuth account
|
||||
*/
|
||||
|
||||
Generated
+35
-117
@@ -67,7 +67,7 @@ importers:
|
||||
version: 24.12.2
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
byte-size:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.1
|
||||
@@ -112,10 +112,10 @@ importers:
|
||||
version: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest:
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest-fetch-mock:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.5(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 0.4.5(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
yaml:
|
||||
specifier: ^2.3.1
|
||||
version: 2.8.3
|
||||
@@ -670,7 +670,7 @@ importers:
|
||||
version: 13.15.10
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
eslint:
|
||||
specifier: ^10.0.0
|
||||
version: 10.1.0(jiti@2.6.1)
|
||||
@@ -727,7 +727,7 @@ importers:
|
||||
version: 6.1.1(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
web:
|
||||
dependencies:
|
||||
@@ -749,6 +749,9 @@ importers:
|
||||
'@mdi/js':
|
||||
specifier: ^7.4.47
|
||||
version: 7.4.47
|
||||
'@noble/hashes':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
'@photo-sphere-viewer/core':
|
||||
specifier: ^5.14.0
|
||||
version: 5.14.1
|
||||
@@ -781,7 +784,7 @@ importers:
|
||||
version: 2.6.0
|
||||
fabric:
|
||||
specifier: ^7.0.0
|
||||
version: 7.2.0
|
||||
version: 7.2.0(encoding@0.1.13)
|
||||
geo-coordinates-parser:
|
||||
specifier: ^1.7.4
|
||||
version: 1.7.4
|
||||
@@ -887,7 +890,7 @@ importers:
|
||||
version: 6.9.1
|
||||
'@testing-library/svelte':
|
||||
specifier: ^5.2.8
|
||||
version: 5.3.1(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 5.3.1(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.5.2
|
||||
version: 14.6.1(@testing-library/dom@10.4.1)
|
||||
@@ -911,7 +914,7 @@ importers:
|
||||
version: 1.5.6
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
dotenv:
|
||||
specifier: ^17.0.0
|
||||
version: 17.3.1
|
||||
@@ -974,7 +977,7 @@ importers:
|
||||
version: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest:
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -3585,6 +3588,10 @@ packages:
|
||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@noble/hashes@2.2.0':
|
||||
resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -15549,22 +15556,6 @@ snapshots:
|
||||
|
||||
'@mapbox/mapbox-gl-rtl-text@0.3.0': {}
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
https-proxy-agent: 5.0.1
|
||||
make-dir: 3.1.0
|
||||
node-fetch: 2.7.0
|
||||
nopt: 5.0.0
|
||||
npmlog: 5.0.1
|
||||
rimraf: 3.0.2
|
||||
semver: 7.7.4
|
||||
tar: 6.2.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
@@ -15871,6 +15862,8 @@ snapshots:
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
'@noble/hashes@2.2.0': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -16950,14 +16943,14 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.55.1
|
||||
|
||||
'@testing-library/svelte@5.3.1(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
'@testing-library/svelte@5.3.1(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
'@testing-library/svelte-core': 1.0.0(svelte@5.55.1)
|
||||
svelte: 5.55.1
|
||||
optionalDependencies:
|
||||
vite: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
|
||||
dependencies:
|
||||
@@ -17652,7 +17645,7 @@ snapshots:
|
||||
|
||||
'@vercel/oidc@3.0.5': {}
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
@@ -17667,11 +17660,11 @@ snapshots:
|
||||
std-env: 3.10.0
|
||||
test-exclude: 7.0.2
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
'@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.1.2
|
||||
@@ -17683,9 +17676,9 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 4.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
'@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
'@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.1.2
|
||||
@@ -17697,7 +17690,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 4.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
'@vitest/expect@3.2.4':
|
||||
dependencies:
|
||||
@@ -18473,16 +18466,6 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001776: {}
|
||||
|
||||
canvas@2.11.2:
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 1.0.11
|
||||
nan: 2.26.2
|
||||
simple-get: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
canvas@2.11.2(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
|
||||
@@ -20114,10 +20097,10 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fabric@7.2.0:
|
||||
fabric@7.2.0(encoding@0.1.13):
|
||||
optionalDependencies:
|
||||
canvas: 2.11.2
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
canvas: 2.11.2(encoding@0.1.13)
|
||||
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- encoding
|
||||
@@ -21284,36 +21267,6 @@ snapshots:
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
jsdom@26.1.0(canvas@2.11.2):
|
||||
dependencies:
|
||||
cssstyle: 4.6.0
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.23
|
||||
parse5: 7.3.0
|
||||
rrweb-cssom: 0.8.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 5.1.2
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
ws: 8.20.0
|
||||
xml-name-validator: 5.0.0
|
||||
optionalDependencies:
|
||||
canvas: 2.11.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
jsep@1.4.0: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
@@ -22555,11 +22508,6 @@ snapshots:
|
||||
emojilib: 2.4.0
|
||||
skin-tone: 2.0.0
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
optional: true
|
||||
|
||||
node-fetch@2.7.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
@@ -25700,11 +25648,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vitest-fetch-mock@0.4.5(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))):
|
||||
vitest-fetch-mock@0.4.5(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))):
|
||||
dependencies:
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
@@ -25733,7 +25681,7 @@ snapshots:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 24.12.2
|
||||
happy-dom: 20.8.9
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
@@ -25778,37 +25726,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.2
|
||||
'@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vitest/pretty-format': 4.1.2
|
||||
'@vitest/runner': 4.1.2
|
||||
'@vitest/snapshot': 4.1.2
|
||||
'@vitest/spy': 4.1.2
|
||||
'@vitest/utils': 4.1.2
|
||||
es-module-lexer: 2.0.0
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.4
|
||||
std-env: 4.0.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.0.4
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/node': 24.12.2
|
||||
happy-dom: 20.8.9
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.2
|
||||
'@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
@@ -25834,7 +25752,7 @@ snapshots:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/node': 25.5.0
|
||||
happy-dom: 20.8.9
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { json } from 'body-parser';
|
||||
import { json, urlencoded } from 'body-parser';
|
||||
import compression from 'compression';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import helmetMiddleware from 'helmet';
|
||||
@@ -56,6 +56,7 @@ export async function configureExpress(
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(json({ limit: '10mb' }));
|
||||
app.use(urlencoded({ limit: '10mb' }));
|
||||
|
||||
if (configRepository.isDev()) {
|
||||
app.enableCors();
|
||||
|
||||
@@ -104,8 +104,10 @@ export type SystemConfig = {
|
||||
defaultStorageQuota: number | null;
|
||||
enabled: boolean;
|
||||
issuerUrl: string;
|
||||
endSessionEndpoint: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
prompt: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
profileSigningAlgorithm: string;
|
||||
@@ -296,8 +298,10 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
defaultStorageQuota: null,
|
||||
enabled: false,
|
||||
issuerUrl: '',
|
||||
endSessionEndpoint: '',
|
||||
mobileOverrideEnabled: false,
|
||||
mobileRedirectUri: '',
|
||||
prompt: '',
|
||||
scope: 'openid email profile',
|
||||
signingAlgorithm: 'RS256',
|
||||
profileSigningAlgorithm: 'none',
|
||||
|
||||
@@ -118,6 +118,7 @@ describe(AuthController.name, () => {
|
||||
expect(service.login).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ email: 'admin@immich.app' }),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -129,7 +130,49 @@ describe(AuthController.name, () => {
|
||||
.send({ name: 'admin', email: 'admin@local', password: 'password' });
|
||||
|
||||
expect(status).toEqual(201);
|
||||
expect(service.login).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@local' }), expect.anything());
|
||||
expect(service.login).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ email: 'admin@local' }),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear the link token cookie on successful login when it was present', async () => {
|
||||
const loginResponse = mediumFactory.loginResponse();
|
||||
service.login.mockResolvedValue(loginResponse);
|
||||
|
||||
const { status, headers } = await request(ctx.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.set('Cookie', 'immich_oauth_link_token=plain')
|
||||
.send({ name: 'admin', email: 'admin@local', password: 'password' });
|
||||
|
||||
expect(status).toEqual(201);
|
||||
const cookies = (headers['set-cookie'] as unknown as string[]).join('\n');
|
||||
expect(cookies).toMatch(/immich_oauth_link_token=;/);
|
||||
});
|
||||
|
||||
it('should clear the link token cookie when login fails', async () => {
|
||||
service.login.mockRejectedValue(new Error('Incorrect email or password'));
|
||||
|
||||
const { headers } = await request(ctx.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.set('Cookie', 'immich_oauth_link_token=plain')
|
||||
.send({ name: 'admin', email: 'admin@local', password: 'wrong' });
|
||||
|
||||
const cookies = (headers['set-cookie'] as unknown as string[] | undefined)?.join('\n') ?? '';
|
||||
expect(cookies).toMatch(/immich_oauth_link_token=;/);
|
||||
});
|
||||
|
||||
it('should not set a link token cookie header when no link token was present', async () => {
|
||||
const loginResponse = mediumFactory.loginResponse();
|
||||
service.login.mockResolvedValue(loginResponse);
|
||||
|
||||
const { headers } = await request(ctx.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ name: 'admin', email: 'admin@local', password: 'password' });
|
||||
|
||||
const cookies = (headers['set-cookie'] as unknown as string[]).join('\n');
|
||||
expect(cookies).not.toMatch(/immich_oauth_link_token=/);
|
||||
});
|
||||
|
||||
it('should auth cookies on a secure connection', async () => {
|
||||
@@ -170,6 +213,33 @@ describe(AuthController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/register', () => {
|
||||
it('should clear the link token cookie on successful register', async () => {
|
||||
const loginResponse = mediumFactory.loginResponse();
|
||||
service.register.mockResolvedValue(loginResponse);
|
||||
|
||||
const { headers } = await request(ctx.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.set('Cookie', 'immich_oauth_link_token=plain')
|
||||
.send({});
|
||||
|
||||
const cookies = (headers['set-cookie'] as unknown as string[]).join('\n');
|
||||
expect(cookies).toMatch(/immich_oauth_link_token=;/);
|
||||
});
|
||||
|
||||
it('should clear the link token cookie when register fails', async () => {
|
||||
service.register.mockRejectedValue(new Error('Missing OAuth link token'));
|
||||
|
||||
const { headers } = await request(ctx.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.set('Cookie', 'immich_oauth_link_token=plain')
|
||||
.send({});
|
||||
|
||||
const cookies = (headers['set-cookie'] as unknown as string[] | undefined)?.join('\n') ?? '';
|
||||
expect(cookies).toMatch(/immich_oauth_link_token=;/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/logout', () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/auth/logout');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Post, Put, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { parse as parseCookie } from 'cookie';
|
||||
import { Request, Response } from 'express';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import {
|
||||
@@ -34,19 +35,56 @@ export class AuthController {
|
||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||
})
|
||||
async login(
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Body() loginCredential: LoginCredentialDto,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<LoginResponseDto> {
|
||||
const body = await this.service.login(loginCredential, loginDetails);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [
|
||||
{ key: ImmichCookie.AccessToken, value: body.accessToken },
|
||||
{ key: ImmichCookie.AuthType, value: AuthType.Password },
|
||||
{ key: ImmichCookie.IsAuthenticated, value: 'true' },
|
||||
],
|
||||
});
|
||||
const hadLinkCookie = !!parseCookie(request.headers.cookie || '')[ImmichCookie.OAuthLinkToken];
|
||||
try {
|
||||
const body = await this.service.login(loginCredential, loginDetails, request.headers);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [
|
||||
{ key: ImmichCookie.AccessToken, value: body.accessToken },
|
||||
{ key: ImmichCookie.AuthType, value: AuthType.Password },
|
||||
{ key: ImmichCookie.IsAuthenticated, value: 'true' },
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
if (hadLinkCookie) {
|
||||
res.clearCookie(ImmichCookie.OAuthLinkToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Post('register')
|
||||
@Endpoint({
|
||||
summary: 'Register via OAuth',
|
||||
description: 'Create a new user from a pending OAuth link token (requires OAuth auto-register to be enabled).',
|
||||
history: new HistoryBuilder().added('v2'),
|
||||
})
|
||||
async register(
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<LoginResponseDto> {
|
||||
const hadLinkCookie = !!parseCookie(request.headers.cookie || '')[ImmichCookie.OAuthLinkToken];
|
||||
try {
|
||||
const body = await this.service.register(loginDetails, request.headers);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [
|
||||
{ key: ImmichCookie.AccessToken, value: body.accessToken },
|
||||
{ key: ImmichCookie.AuthType, value: AuthType.OAuth },
|
||||
{ key: ImmichCookie.IsAuthenticated, value: 'true' },
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
if (hadLinkCookie) {
|
||||
res.clearCookie(ImmichCookie.OAuthLinkToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Post('admin-sign-up')
|
||||
|
||||
@@ -96,6 +96,12 @@ describe(MemoryController.name, () => {
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined']));
|
||||
});
|
||||
|
||||
it('should require at least one field', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`).send({});
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest(['At least one field must be provided']));
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /memories/:id', () => {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { parse as parseCookie } from 'cookie';
|
||||
import { Request, Response } from 'express';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import {
|
||||
AuthDto,
|
||||
LoginResponseDto,
|
||||
OAuthAuthorizeResponseDto,
|
||||
OAuthBackchannelLogoutDto,
|
||||
OAuthCallbackDto,
|
||||
OAuthConfigDto,
|
||||
OAuthReLinkStartDto,
|
||||
} from 'src/dtos/auth.dto';
|
||||
import { UserAdminResponseDto } from 'src/dtos/user.dto';
|
||||
import { ApiTag, AuthType, ImmichCookie } from 'src/enum';
|
||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
import { AuthService, LoginDetails, OAuthLinkRequiredException } from 'src/services/auth.service';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
|
||||
@ApiTags(ApiTag.Authentication)
|
||||
@@ -72,33 +75,54 @@ export class OAuthController {
|
||||
@Body() dto: OAuthCallbackDto,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<LoginResponseDto> {
|
||||
const body = await this.service.callback(dto, request.headers, loginDetails);
|
||||
res.clearCookie(ImmichCookie.OAuthState);
|
||||
res.clearCookie(ImmichCookie.OAuthCodeVerifier);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [
|
||||
{ key: ImmichCookie.AccessToken, value: body.accessToken },
|
||||
{ key: ImmichCookie.AuthType, value: AuthType.OAuth },
|
||||
{ key: ImmichCookie.IsAuthenticated, value: 'true' },
|
||||
],
|
||||
});
|
||||
const hadLinkCookie = !!parseCookie(request.headers.cookie || '')[ImmichCookie.OAuthLinkToken];
|
||||
let freshLinkCookieIssued = false;
|
||||
try {
|
||||
const body = await this.service.callback(dto, request.headers, loginDetails);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [
|
||||
{ key: ImmichCookie.AccessToken, value: body.accessToken },
|
||||
{ key: ImmichCookie.AuthType, value: AuthType.OAuth },
|
||||
{ key: ImmichCookie.IsAuthenticated, value: 'true' },
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof OAuthLinkRequiredException) {
|
||||
respondWithCookie(res, null, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [{ key: ImmichCookie.OAuthLinkToken, value: error.oauthLinkToken }],
|
||||
});
|
||||
freshLinkCookieIssued = true;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
res.clearCookie(ImmichCookie.OAuthState);
|
||||
res.clearCookie(ImmichCookie.OAuthCodeVerifier);
|
||||
if (hadLinkCookie && !freshLinkCookieIssued) {
|
||||
res.clearCookie(ImmichCookie.OAuthLinkToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Post('link')
|
||||
@Authenticated()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('relink-start')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Endpoint({
|
||||
summary: 'Link OAuth account',
|
||||
description: 'Link an OAuth account to the authenticated user.',
|
||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||
summary: 'Start OAuth re-link',
|
||||
description:
|
||||
'Redeem an admin-issued OAuth re-link token, setting a short-lived cookie that gets consumed by the subsequent OAuth callback.',
|
||||
history: new HistoryBuilder().added('v2'),
|
||||
})
|
||||
linkOAuthAccount(
|
||||
@Req() request: Request,
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: OAuthCallbackDto,
|
||||
): Promise<UserAdminResponseDto> {
|
||||
return this.service.link(auth, dto, request.headers);
|
||||
async startOAuthReLink(
|
||||
@Body() dto: OAuthReLinkStartDto,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<void> {
|
||||
await this.service.validateOAuthReLinkToken(dto.token);
|
||||
respondWithCookie(res, null, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [{ key: ImmichCookie.OAuthLinkToken, value: dto.token }],
|
||||
});
|
||||
}
|
||||
|
||||
@Post('unlink')
|
||||
@@ -112,4 +136,17 @@ export class OAuthController {
|
||||
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> {
|
||||
return this.service.unlink(auth);
|
||||
}
|
||||
|
||||
@Post('backchannel-logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiConsumes('application/x-www-form-urlencoded')
|
||||
@Endpoint({
|
||||
summary: 'Backchannel OAuth logout',
|
||||
description:
|
||||
'Logout the OAuth account and invalidate the session specified by the sid claim or all sessions if the sid claim is not present.',
|
||||
history: new HistoryBuilder().added('v2'),
|
||||
})
|
||||
async logoutOAuth(@Body() dto: OAuthBackchannelLogoutDto): Promise<void> {
|
||||
return this.service.backchannelLogout(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionResponseDto } from 'src/dtos/session.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||
import {
|
||||
OAuthReLinkTokenResponseDto,
|
||||
UserAdminCreateDto,
|
||||
UserAdminDeleteDto,
|
||||
UserAdminResponseDto,
|
||||
@@ -137,6 +138,21 @@ export class UserAdminController {
|
||||
return this.service.updatePreferences(auth, id, dto);
|
||||
}
|
||||
|
||||
@Post(':id/oauth-relink-token')
|
||||
@Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
|
||||
@Endpoint({
|
||||
summary: 'Issue an OAuth re-link token',
|
||||
description:
|
||||
'Create a single-use token that lets a user re-link their account to a new OAuth sub (e.g. when migrating IdPs). Deliver the token to the user out-of-band.',
|
||||
history: new HistoryBuilder().added('v2'),
|
||||
})
|
||||
createOAuthReLinkTokenAdmin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
): Promise<OAuthReLinkTokenResponseDto> {
|
||||
return this.service.createOAuthReLinkToken(auth, id);
|
||||
}
|
||||
|
||||
@Post(':id/restore')
|
||||
@Authenticated({ permission: Permission.AdminUserDelete, admin: true })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
|
||||
@@ -124,6 +124,16 @@ const OAuthAuthorizeResponseSchema = z
|
||||
})
|
||||
.meta({ id: 'OAuthAuthorizeResponseDto' });
|
||||
|
||||
const OAuthBackchannelLogoutSchema = z
|
||||
.object({ logout_token: z.string().describe('OAuth logout token') })
|
||||
.meta({ id: 'OAuthBackchannelLogoutDto' });
|
||||
|
||||
const OAuthReLinkStartSchema = z
|
||||
.object({
|
||||
token: z.string().describe('Plaintext OAuth re-link token issued by an administrator'),
|
||||
})
|
||||
.meta({ id: 'OAuthReLinkStartDto' });
|
||||
|
||||
const AuthStatusResponseSchema = z
|
||||
.object({
|
||||
pinCode: z.boolean().describe('Has PIN code set'),
|
||||
@@ -147,4 +157,6 @@ export class ValidateAccessTokenResponseDto extends createZodDto(ValidateAccessT
|
||||
export class OAuthCallbackDto extends createZodDto(OAuthCallbackSchema) {}
|
||||
export class OAuthConfigDto extends createZodDto(OAuthConfigSchema) {}
|
||||
export class OAuthAuthorizeResponseDto extends createZodDto(OAuthAuthorizeResponseSchema) {}
|
||||
export class OAuthBackchannelLogoutDto extends createZodDto(OAuthBackchannelLogoutSchema) {}
|
||||
export class OAuthReLinkStartDto extends createZodDto(OAuthReLinkStartSchema) {}
|
||||
export class AuthStatusResponseDto extends createZodDto(AuthStatusResponseSchema) {}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HistoryBuilder } from 'src/decorators';
|
||||
import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetOrderWithRandomSchema, MemoryType, MemoryTypeSchema } from 'src/enum';
|
||||
import { isoDatetimeToDate, stringToBool } from 'src/validation';
|
||||
import { isoDatetimeToDate, nonEmptyPartial, stringToBool } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const MemorySearchSchema = z
|
||||
@@ -26,13 +26,11 @@ const OnThisDaySchema = z
|
||||
|
||||
type MemoryData = z.infer<typeof OnThisDaySchema>;
|
||||
|
||||
const MemoryUpdateSchema = z
|
||||
.object({
|
||||
isSaved: z.boolean().optional().describe('Is memory saved'),
|
||||
seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'),
|
||||
memoryAt: isoDatetimeToDate.optional().describe('Memory date'),
|
||||
})
|
||||
.meta({ id: 'MemoryUpdateDto' });
|
||||
const MemoryUpdateSchema = nonEmptyPartial({
|
||||
isSaved: z.boolean().describe('Is memory saved'),
|
||||
seenAt: isoDatetimeToDate.describe('Date when memory was seen'),
|
||||
memoryAt: isoDatetimeToDate.describe('Memory date'),
|
||||
}).meta({ id: 'MemoryUpdateDto' });
|
||||
|
||||
const MemoryCreateSchema = z
|
||||
.object({
|
||||
|
||||
@@ -132,6 +132,7 @@ const ServerFeaturesSchema = z
|
||||
importFaces: z.boolean().describe('Whether face import is enabled'),
|
||||
oauth: z.boolean().describe('Whether OAuth is enabled'),
|
||||
oauthAutoLaunch: z.boolean().describe('Whether OAuth auto-launch is enabled'),
|
||||
oauthAutoRegister: z.boolean().describe('Whether OAuth auto-register is enabled'),
|
||||
passwordLogin: z.boolean().describe('Whether password login is enabled'),
|
||||
sidecar: z.boolean().describe('Whether sidecar files are supported'),
|
||||
search: z.boolean().describe('Whether search is enabled'),
|
||||
|
||||
@@ -189,6 +189,13 @@ const SystemConfigOAuthSchema = z
|
||||
})
|
||||
.describe('Issuer URL'),
|
||||
scope: z.string().describe('Scope'),
|
||||
prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'),
|
||||
endSessionEndpoint: z
|
||||
.string()
|
||||
.refine((url) => url.length === 0 || z.url().safeParse(url).success, {
|
||||
error: 'endSessionEndpoint must be an empty string or a valid URL',
|
||||
})
|
||||
.describe('End session endpoint'),
|
||||
signingAlgorithm: z.string().describe('Signing algorithm'),
|
||||
profileSigningAlgorithm: z.string().describe('Profile signing algorithm'),
|
||||
storageLabelClaim: z.string().describe('Storage label claim'),
|
||||
|
||||
@@ -121,6 +121,15 @@ const UserAdminDeleteSchema = z
|
||||
|
||||
export class UserAdminDeleteDto extends createZodDto(UserAdminDeleteSchema) {}
|
||||
|
||||
const OAuthReLinkTokenResponseSchema = z
|
||||
.object({
|
||||
token: z.string().describe('Single-use token; deliver to the user via /auth/link?token=<token>'),
|
||||
expiresAt: isoDatetimeToDate.describe('Token expiration'),
|
||||
})
|
||||
.meta({ id: 'OAuthReLinkTokenResponseDto' });
|
||||
|
||||
export class OAuthReLinkTokenResponseDto extends createZodDto(OAuthReLinkTokenResponseSchema) {}
|
||||
|
||||
const UserAdminResponseSchema = UserResponseSchema.extend({
|
||||
storageLabel: z.string().nullable().describe('Storage label'),
|
||||
shouldChangePassword: z.boolean().describe('Require password change on next login'),
|
||||
|
||||
@@ -15,6 +15,7 @@ export enum ImmichCookie {
|
||||
SharedLinkToken = 'immich_shared_link_token',
|
||||
OAuthState = 'immich_oauth_state',
|
||||
OAuthCodeVerifier = 'immich_oauth_code_verifier',
|
||||
OAuthLinkToken = 'immich_oauth_link_token',
|
||||
}
|
||||
|
||||
export const ImmichCookieSchema = z.enum(ImmichCookie).describe('Immich cookie').meta({ id: 'ImmichCookie' });
|
||||
|
||||
@@ -74,7 +74,7 @@ delete from "session"
|
||||
where
|
||||
"id" = $1::uuid
|
||||
|
||||
-- SessionRepository.invalidate
|
||||
-- SessionRepository.invalidateAll
|
||||
delete from "session"
|
||||
where
|
||||
"userId" = $1
|
||||
|
||||
@@ -25,6 +25,7 @@ import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthLinkTokenRepository } from 'src/repositories/oauth-link-token.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
@@ -78,6 +79,7 @@ export const repositories = [
|
||||
MetadataRepository,
|
||||
MoveRepository,
|
||||
NotificationRepository,
|
||||
OAuthLinkTokenRepository,
|
||||
OAuthRepository,
|
||||
OcrRepository,
|
||||
PartnerRepository,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely } from 'kysely';
|
||||
import { DateTime } from 'luxon';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/schema';
|
||||
import { OAuthLinkTokenTable } from 'src/schema/tables/oauth-link-token.table';
|
||||
|
||||
@Injectable()
|
||||
export class OAuthLinkTokenRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
create(dto: Insertable<OAuthLinkTokenTable>) {
|
||||
return this.db.insertInto('oauth_link_token').values(dto).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
getByToken(token: Buffer) {
|
||||
return this.db
|
||||
.selectFrom('oauth_link_token')
|
||||
.selectAll()
|
||||
.where('token', '=', token)
|
||||
.where('expiresAt', '>', DateTime.now().toJSDate())
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
consumeToken(token: Buffer, kind: 'callback' | 'admin' | 'any' = 'any') {
|
||||
let query = this.db
|
||||
.deleteFrom('oauth_link_token')
|
||||
.where('token', '=', token)
|
||||
.where('expiresAt', '>', DateTime.now().toJSDate());
|
||||
if (kind === 'callback') {
|
||||
query = query.where('oauthSub', 'is not', null);
|
||||
} else if (kind === 'admin') {
|
||||
query = query.where('oauthSub', 'is', null);
|
||||
}
|
||||
return query.returningAll().executeTakeFirst();
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
const result = await this.db
|
||||
.deleteFrom('oauth_link_token')
|
||||
.where('expiresAt', '<=', DateTime.now().toJSDate())
|
||||
.execute();
|
||||
return Number(result[0]?.numDeletedRows ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { createRemoteJWKSet, jwtVerify, JWTVerifyGetKey } from 'jose';
|
||||
import {
|
||||
allowInsecureRequests as allowInsecureRequestsExecute,
|
||||
authorizationCodeGrant,
|
||||
@@ -21,9 +22,11 @@ export type OAuthConfig = {
|
||||
clientId: string;
|
||||
clientSecret?: string;
|
||||
issuerUrl: string;
|
||||
endSessionEndpoint: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
profileSigningAlgorithm: string;
|
||||
prompt: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
|
||||
@@ -56,6 +59,10 @@ export class OAuthRepository {
|
||||
state,
|
||||
};
|
||||
|
||||
if (config.prompt) {
|
||||
params.prompt = config.prompt;
|
||||
}
|
||||
|
||||
if (client.serverMetadata().supportsPKCE()) {
|
||||
params.code_challenge = codeChallenge;
|
||||
params.code_challenge_method = 'S256';
|
||||
@@ -71,12 +78,12 @@ export class OAuthRepository {
|
||||
return client.serverMetadata().end_session_endpoint;
|
||||
}
|
||||
|
||||
async getProfile(
|
||||
async getProfileAndOAuthSid(
|
||||
config: OAuthConfig,
|
||||
url: string,
|
||||
expectedState: string,
|
||||
codeVerifier: string,
|
||||
): Promise<OAuthProfile> {
|
||||
): Promise<{ profile: OAuthProfile; sid?: string }> {
|
||||
const client = await this.getClient(config);
|
||||
const pkceCodeVerifier = client.serverMetadata().supportsPKCE() ? codeVerifier : undefined;
|
||||
|
||||
@@ -96,7 +103,15 @@ export class OAuthRepository {
|
||||
throw new Error('Unexpected profile response, no `sub`');
|
||||
}
|
||||
|
||||
return profile;
|
||||
let sid: string | undefined;
|
||||
if (tokens.id_token) {
|
||||
const claims = tokens.claims();
|
||||
if (typeof claims?.sid === 'string') {
|
||||
sid = claims.sid;
|
||||
}
|
||||
}
|
||||
|
||||
return { profile, sid };
|
||||
} catch (error: Error | any) {
|
||||
if (error.message.includes('unexpected JWT alg received')) {
|
||||
this.logger.warn(
|
||||
@@ -126,6 +141,59 @@ export class OAuthRepository {
|
||||
};
|
||||
}
|
||||
|
||||
private jwksClients: Map<string, JWTVerifyGetKey> = new Map(); // useful for caching and performnce
|
||||
async validateLogoutToken(config: OAuthConfig, logoutToken: string): Promise<{ sub?: string; sid?: string } | null> {
|
||||
const client = await this.getClient(config);
|
||||
const algorithm = client.clientMetadata().id_token_signed_response_alg ?? 'RS256';
|
||||
let keyOrGetter: Uint8Array | JWTVerifyGetKey;
|
||||
|
||||
try {
|
||||
if (algorithm.startsWith('HS')) {
|
||||
keyOrGetter = new TextEncoder().encode(config.clientSecret);
|
||||
} else {
|
||||
const jwksUri = client.serverMetadata().jwks_uri;
|
||||
if (!jwksUri) {
|
||||
throw new Error('Unable to get JWKS URI');
|
||||
}
|
||||
|
||||
if (!this.jwksClients.has(jwksUri)) {
|
||||
this.jwksClients.set(jwksUri, createRemoteJWKSet(new URL(jwksUri)));
|
||||
}
|
||||
keyOrGetter = this.jwksClients.get(jwksUri) as JWTVerifyGetKey;
|
||||
}
|
||||
|
||||
const { payload } = await jwtVerify(logoutToken, keyOrGetter as any, {
|
||||
issuer: client.serverMetadata().issuer,
|
||||
audience: config.clientId,
|
||||
algorithms: [algorithm],
|
||||
maxTokenAge: '2m',
|
||||
clockTolerance: '5s',
|
||||
});
|
||||
|
||||
// Validate specific Logout Token claims (RFC 8963):
|
||||
// "events" claim must exist and contain the backchannel-logout event
|
||||
const events = payload.events as Record<string, any> | undefined;
|
||||
if (!events || !events['http://schemas.openid.net/event/backchannel-logout']) {
|
||||
throw new Error('Missing backchannel-logout event claim');
|
||||
}
|
||||
|
||||
// "nonce" must not be present
|
||||
if (payload.nonce) {
|
||||
throw new Error('Logout token must not contain a nonce');
|
||||
}
|
||||
|
||||
return {
|
||||
sub: payload.sub,
|
||||
sid: payload.sid as string | undefined,
|
||||
};
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(`Error validating JWT logout token: ${error.message}`);
|
||||
this.logger.error(error);
|
||||
|
||||
throw new Error('Error validating JWT logout token', { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
private async getClient({
|
||||
issuerUrl,
|
||||
clientId,
|
||||
|
||||
@@ -102,7 +102,7 @@ export class SessionRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, excludeId: DummyValue.UUID }] })
|
||||
async invalidate({ userId, excludeId }: { userId: string; excludeId?: string }) {
|
||||
async invalidateAll({ userId, excludeId }: { userId: string; excludeId?: string }) {
|
||||
await this.db
|
||||
.deleteFrom('session')
|
||||
.where('userId', '=', userId)
|
||||
@@ -110,6 +110,28 @@ export class SessionRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING, DummyValue.STRING] })
|
||||
async invalidateOAuth({ oauthSid, oauthId }: { oauthSid?: string; oauthId?: string }): Promise<string[]> {
|
||||
let query = this.db.deleteFrom('session').returning('session.id');
|
||||
|
||||
if (oauthSid && oauthId) {
|
||||
query = query
|
||||
.using('user')
|
||||
.whereRef('user.id', '=', 'session.userId')
|
||||
.where('session.oauthSid', '=', oauthSid)
|
||||
.where('user.oauthId', '=', oauthId);
|
||||
} else if (!oauthSid && oauthId) {
|
||||
query = query.using('user').whereRef('user.id', '=', 'session.userId').where('user.oauthId', '=', oauthId);
|
||||
} else if (oauthSid && !oauthId) {
|
||||
query = query.where('session.oauthSid', '=', oauthSid);
|
||||
} else {
|
||||
throw new Error('Invalid arguments: at least one of oauthSid or oauthId must be present');
|
||||
}
|
||||
|
||||
const deletedRows = await query.execute();
|
||||
return deletedRows.map((row) => row.id);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async lockAll(userId: string) {
|
||||
await this.db.updateTable('session').set({ pinExpiresAt: null }).where('userId', '=', userId).execute();
|
||||
|
||||
@@ -50,6 +50,7 @@ import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { MoveTable } from 'src/schema/tables/move.table';
|
||||
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
||||
import { NotificationTable } from 'src/schema/tables/notification.table';
|
||||
import { OAuthLinkTokenTable } from 'src/schema/tables/oauth-link-token.table';
|
||||
import { OcrSearchTable } from 'src/schema/tables/ocr-search.table';
|
||||
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
||||
import { PartnerTable } from 'src/schema/tables/partner.table';
|
||||
@@ -108,6 +109,7 @@ export class ImmichDatabase {
|
||||
MoveTable,
|
||||
NaturalEarthCountriesTable,
|
||||
NotificationTable,
|
||||
OAuthLinkTokenTable,
|
||||
OcrSearchTable,
|
||||
PartnerAuditTable,
|
||||
PartnerTable,
|
||||
@@ -210,6 +212,8 @@ export interface DB {
|
||||
|
||||
notification: NotificationTable;
|
||||
|
||||
oauth_link_token: OAuthLinkTokenTable;
|
||||
|
||||
move_history: MoveTable;
|
||||
|
||||
naturalearth_countries: NaturalEarthCountriesTable;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`
|
||||
CREATE TABLE "oauth_link_token" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"token" bytea NOT NULL,
|
||||
"oauthSub" varchar,
|
||||
"oauthSid" varchar,
|
||||
"email" varchar NOT NULL,
|
||||
"profile" jsonb,
|
||||
"expiresAt" timestamp with time zone NOT NULL,
|
||||
"createdAt" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
`.execute(db);
|
||||
|
||||
await sql`ALTER TABLE "oauth_link_token" ADD CONSTRAINT "oauth_link_token_pkey" PRIMARY KEY ("id");`.execute(db);
|
||||
await sql`CREATE INDEX "oauth_link_token_token_idx" ON "oauth_link_token" ("token")`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP TABLE IF EXISTS "oauth_link_token";`.execute(db);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "session" ADD "oauthSid" character varying;`.execute(db);
|
||||
await sql`CREATE INDEX "session_oauthSid_idx" ON "session" ("oauthSid");`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP INDEX "session_oauthSid_idx";`.execute(db);
|
||||
await sql`ALTER TABLE "session" DROP COLUMN "oauthSid";`.execute(db);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Column, CreateDateColumn, Generated, PrimaryGeneratedColumn, Table, Timestamp } from '@immich/sql-tools';
|
||||
|
||||
export type OAuthLinkTokenProfile = {
|
||||
name: string;
|
||||
storageLabel: string | null;
|
||||
storageQuotaInGiB: number | null;
|
||||
isAdmin: boolean;
|
||||
picture: string | null;
|
||||
};
|
||||
|
||||
@Table({ name: 'oauth_link_token' })
|
||||
export class OAuthLinkTokenTable {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: Generated<string>;
|
||||
|
||||
@Column({ type: 'bytea', index: true })
|
||||
token!: Buffer;
|
||||
|
||||
@Column({ nullable: true })
|
||||
oauthSub!: string | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
oauthSid!: string | null;
|
||||
|
||||
@Column()
|
||||
email!: string;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
profile!: OAuthLinkTokenProfile | null;
|
||||
|
||||
@Column({ type: 'timestamp with time zone' })
|
||||
expiresAt!: Timestamp;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt!: Generated<Timestamp>;
|
||||
}
|
||||
@@ -52,4 +52,7 @@ export class SessionTable {
|
||||
|
||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||
pinExpiresAt!: Timestamp | null;
|
||||
|
||||
@Column({ nullable: true, index: true })
|
||||
oauthSid!: string | null;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,9 @@ import { BadRequestException, ForbiddenException, Injectable, UnauthorizedExcept
|
||||
import { parse } from 'cookie';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
import { join } from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
|
||||
import {
|
||||
AuthDto,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ChangePasswordDto,
|
||||
LoginCredentialDto,
|
||||
LogoutResponseDto,
|
||||
OAuthBackchannelLogoutDto,
|
||||
OAuthCallbackDto,
|
||||
OAuthConfigDto,
|
||||
PinCodeChangeDto,
|
||||
@@ -22,12 +23,13 @@ import {
|
||||
mapLoginResponse,
|
||||
} from 'src/dtos/auth.dto';
|
||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, JobName, Permission, StorageFolder } from 'src/enum';
|
||||
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, JobName, Permission } from 'src/enum';
|
||||
import { OAuthProfile } from 'src/repositories/oauth.repository';
|
||||
import { OAuthLinkTokenProfile } from 'src/schema/tables/oauth-link-token.table';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { isGranted } from 'src/utils/access';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { generateProfileImage } from 'src/utils/profile-image';
|
||||
import { getUserAgentDetails } from 'src/utils/request';
|
||||
export interface LoginDetails {
|
||||
isSecure: boolean;
|
||||
@@ -43,6 +45,15 @@ interface ClaimOptions<T> {
|
||||
isValid: (value: unknown) => boolean;
|
||||
}
|
||||
|
||||
export class OAuthLinkRequiredException extends ForbiddenException {
|
||||
constructor(
|
||||
public readonly userEmail: string,
|
||||
public readonly oauthLinkToken: string,
|
||||
) {
|
||||
super({ message: 'oauth_account_link_required', userEmail });
|
||||
}
|
||||
}
|
||||
|
||||
export type ValidateRequest = {
|
||||
headers: IncomingHttpHeaders;
|
||||
queryParams: Record<string, string>;
|
||||
@@ -57,7 +68,7 @@ export type ValidateRequest = {
|
||||
|
||||
@Injectable()
|
||||
export class AuthService extends BaseService {
|
||||
async login(dto: LoginCredentialDto, details: LoginDetails) {
|
||||
async login(dto: LoginCredentialDto, details: LoginDetails, headers: IncomingHttpHeaders) {
|
||||
const config = await this.getConfig({ withCache: false });
|
||||
if (!config.passwordLogin.enabled) {
|
||||
throw new UnauthorizedException('Password login has been disabled');
|
||||
@@ -76,7 +87,55 @@ export class AuthService extends BaseService {
|
||||
throw new UnauthorizedException('Incorrect email or password');
|
||||
}
|
||||
|
||||
return this.createLoginResponse(user, details);
|
||||
let linkedOAuthSid: string | undefined;
|
||||
const linkTokenCookie = this.getCookieOAuthLinkToken(headers);
|
||||
if (linkTokenCookie) {
|
||||
const hashedToken = this.cryptoRepository.hashSha256(linkTokenCookie);
|
||||
const record = await this.oauthLinkTokenRepository.consumeToken(hashedToken, 'callback');
|
||||
if (record && record.oauthSub !== null && record.profile !== null) {
|
||||
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.');
|
||||
}
|
||||
user = await this.applyOAuthProfileToUser(user, { oauthSub: record.oauthSub, profile: record.profile });
|
||||
linkedOAuthSid = record.oauthSid ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this.createLoginResponse(user, details, linkedOAuthSid);
|
||||
}
|
||||
|
||||
async register(details: LoginDetails, headers: IncomingHttpHeaders) {
|
||||
const { oauth } = await this.getConfig({ withCache: false });
|
||||
if (!oauth.enabled || !oauth.autoRegister) {
|
||||
throw new BadRequestException('OAuth auto-register is disabled');
|
||||
}
|
||||
|
||||
const linkTokenCookie = this.getCookieOAuthLinkToken(headers);
|
||||
if (!linkTokenCookie) {
|
||||
throw new BadRequestException('Missing OAuth link token');
|
||||
}
|
||||
|
||||
const hashedToken = this.cryptoRepository.hashSha256(linkTokenCookie);
|
||||
const record = await this.oauthLinkTokenRepository.consumeToken(hashedToken, 'callback');
|
||||
if (!record || record.oauthSub === null || record.profile === null) {
|
||||
throw new BadRequestException('Invalid OAuth link token for registration');
|
||||
}
|
||||
const { oauthSub, profile } = record;
|
||||
|
||||
const existing = await this.userRepository.getByOAuthId(oauthSub);
|
||||
if (existing) {
|
||||
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
||||
}
|
||||
|
||||
this.logger.log(`Registering new user from OAuth: ${oauthSub}/${record.email}`);
|
||||
const newUser = await this.createUser({
|
||||
email: record.email,
|
||||
name: profile.name,
|
||||
isAdmin: profile.isAdmin,
|
||||
});
|
||||
const user = await this.applyOAuthProfileToUser(newUser, { oauthSub, profile });
|
||||
return this.createLoginResponse(user, details, record.oauthSid ?? undefined);
|
||||
}
|
||||
|
||||
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
||||
@@ -91,6 +150,40 @@ export class AuthService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
async backchannelLogout(dto: OAuthBackchannelLogoutDto): Promise<void> {
|
||||
const { oauth } = await this.getConfig({ withCache: false });
|
||||
if (!oauth.enabled) {
|
||||
throw new BadRequestException('Received backchannel logout request but OAuth is not enabled');
|
||||
}
|
||||
|
||||
let claims;
|
||||
try {
|
||||
claims = await this.oauthRepository.validateLogoutToken(oauth, dto.logout_token);
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(`Error backchannel logout: ${error.message}`);
|
||||
this.logger.error(error);
|
||||
|
||||
throw new BadRequestException('Error backchannel logout: token validation failed');
|
||||
}
|
||||
|
||||
if (!claims) {
|
||||
throw new BadRequestException('Invalid logout token: no claims found');
|
||||
}
|
||||
|
||||
if (!claims.sub && !claims.sid) {
|
||||
throw new BadRequestException('Invalid logout token: it must contain either a sub or a sid claim');
|
||||
}
|
||||
|
||||
const deletedSessionIds = await this.sessionRepository.invalidateOAuth({
|
||||
oauthSid: claims.sid,
|
||||
oauthId: claims.sub,
|
||||
});
|
||||
|
||||
for (const sessionId of deletedSessionIds) {
|
||||
await this.eventRepository.emit('SessionDelete', { sessionId });
|
||||
}
|
||||
}
|
||||
|
||||
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
||||
const { password, newPassword } = dto;
|
||||
const user = await this.userRepository.getForChangePassword(auth.user.id);
|
||||
@@ -244,6 +337,19 @@ export class AuthService extends BaseService {
|
||||
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
|
||||
}
|
||||
|
||||
async validateOAuthReLinkToken(plainToken: string) {
|
||||
const { oauth } = await this.getConfig({ withCache: false });
|
||||
if (!oauth.enabled) {
|
||||
throw new BadRequestException('OAuth is not enabled');
|
||||
}
|
||||
|
||||
const hashed = this.cryptoRepository.hashSha256(plainToken);
|
||||
const record = await this.oauthLinkTokenRepository.getByToken(hashed);
|
||||
if (!record || record.oauthSub !== null) {
|
||||
throw new BadRequestException('Invalid or expired re-link token');
|
||||
}
|
||||
}
|
||||
|
||||
async authorize(dto: OAuthConfigDto) {
|
||||
const { oauth } = await this.getConfig({ withCache: false });
|
||||
|
||||
@@ -276,88 +382,134 @@ export class AuthService extends BaseService {
|
||||
}
|
||||
|
||||
const url = this.resolveRedirectUri(oauth, dto.url);
|
||||
const profile = await this.oauthRepository.getProfile(oauth, url, expectedState, codeVerifier);
|
||||
const { profile, sid: oauthSid } = await this.oauthRepository.getProfileAndOAuthSid(
|
||||
oauth,
|
||||
url,
|
||||
expectedState,
|
||||
codeVerifier,
|
||||
);
|
||||
const normalizedEmail = profile.email ? profile.email.trim().toLowerCase() : undefined;
|
||||
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim, roleClaim } = oauth;
|
||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||
let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);
|
||||
const user = await this.userRepository.getByOAuthId(profile.sub);
|
||||
|
||||
// link by email
|
||||
if (!user && normalizedEmail) {
|
||||
const emailUser = await this.userRepository.getByEmail(normalizedEmail);
|
||||
if (emailUser) {
|
||||
if (emailUser.oauthId) {
|
||||
throw new BadRequestException('User already exists, but is linked to another account.');
|
||||
}
|
||||
user = await this.userRepository.update(emailUser.id, { oauthId: profile.sub });
|
||||
if (user) {
|
||||
if (!user.profileImagePath && profile.picture) {
|
||||
await this.syncProfilePicture(user, profile.picture);
|
||||
}
|
||||
return this.createLoginResponse(user, loginDetails, oauthSid);
|
||||
}
|
||||
|
||||
const reLinkTokenCookie = this.getCookieOAuthLinkToken(headers);
|
||||
if (reLinkTokenCookie) {
|
||||
const hashedCookie = this.cryptoRepository.hashSha256(reLinkTokenCookie);
|
||||
const record = await this.oauthLinkTokenRepository.consumeToken(hashedCookie, 'admin');
|
||||
if (record) {
|
||||
return this.completeAdminIssuedReLink(record, profile.sub, oauthSid, loginDetails);
|
||||
}
|
||||
}
|
||||
|
||||
// register new user
|
||||
if (!user) {
|
||||
if (!autoRegister) {
|
||||
this.logger.warn(
|
||||
`Unable to register ${profile.sub}/${normalizedEmail || '(no email)'}. To enable set OAuth Auto Register to true in admin settings.`,
|
||||
);
|
||||
throw new BadRequestException(`User does not exist and auto registering is disabled.`);
|
||||
}
|
||||
|
||||
if (!normalizedEmail) {
|
||||
throw new BadRequestException('OAuth profile does not have an email address');
|
||||
}
|
||||
|
||||
this.logger.log(`Registering new user: ${profile.sub}/${normalizedEmail}`);
|
||||
|
||||
const storageLabel = this.getClaim(profile, {
|
||||
key: storageLabelClaim,
|
||||
default: '',
|
||||
isValid: (value: unknown): value is string => typeof value === 'string',
|
||||
});
|
||||
const storageQuota = this.getClaim(profile, {
|
||||
key: storageQuotaClaim,
|
||||
default: defaultStorageQuota,
|
||||
isValid: (value: unknown) => Number(value) >= 0,
|
||||
});
|
||||
const role = this.getClaim<'admin' | 'user'>(profile, {
|
||||
key: roleClaim,
|
||||
default: 'user',
|
||||
isValid: (value: unknown) => typeof value === 'string' && ['admin', 'user'].includes(value),
|
||||
});
|
||||
|
||||
user = await this.createUser({
|
||||
name:
|
||||
profile.name ||
|
||||
`${profile.given_name || ''} ${profile.family_name || ''}`.trim() ||
|
||||
profile.preferred_username ||
|
||||
normalizedEmail,
|
||||
email: normalizedEmail,
|
||||
oauthId: profile.sub,
|
||||
quotaSizeInBytes: storageQuota === null ? null : storageQuota * HumanReadableSize.GiB,
|
||||
storageLabel: storageLabel || null,
|
||||
isAdmin: role === 'admin',
|
||||
});
|
||||
if (!normalizedEmail) {
|
||||
throw new BadRequestException('OAuth profile does not have an email address');
|
||||
}
|
||||
|
||||
if (!user.profileImagePath && profile.picture) {
|
||||
await this.syncProfilePicture(user, profile.picture);
|
||||
const resolvedProfile = this.resolveOAuthProfile(profile, normalizedEmail, oauth);
|
||||
const plainToken = this.cryptoRepository.randomBytesAsText(32);
|
||||
const hashedToken = this.cryptoRepository.hashSha256(plainToken);
|
||||
await this.oauthLinkTokenRepository.create({
|
||||
token: hashedToken,
|
||||
oauthSub: profile.sub,
|
||||
oauthSid: oauthSid ?? null,
|
||||
email: normalizedEmail,
|
||||
profile: resolvedProfile,
|
||||
expiresAt: DateTime.now().plus({ minutes: 10 }).toJSDate(),
|
||||
});
|
||||
throw new OAuthLinkRequiredException(normalizedEmail, plainToken);
|
||||
}
|
||||
|
||||
private async completeAdminIssuedReLink(
|
||||
record: { email: string },
|
||||
newOAuthSub: string,
|
||||
oauthSid: string | undefined,
|
||||
loginDetails: LoginDetails,
|
||||
) {
|
||||
const targetUser = await this.userRepository.getByEmail(record.email);
|
||||
if (!targetUser) {
|
||||
throw new BadRequestException('The user for this re-link token no longer exists');
|
||||
}
|
||||
|
||||
return this.createLoginResponse(user, loginDetails);
|
||||
const duplicate = await this.userRepository.getByOAuthId(newOAuthSub);
|
||||
if (duplicate && duplicate.id !== targetUser.id) {
|
||||
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
||||
}
|
||||
|
||||
this.logger.log(`Completing admin-issued OAuth re-link for user ${targetUser.id}`);
|
||||
const updated = await this.userRepository.update(targetUser.id, { oauthId: newOAuthSub });
|
||||
return this.createLoginResponse(updated, loginDetails, oauthSid);
|
||||
}
|
||||
|
||||
private resolveOAuthProfile(
|
||||
profile: OAuthProfile,
|
||||
normalizedEmail: string,
|
||||
oauth: SystemConfig['oauth'],
|
||||
): OAuthLinkTokenProfile {
|
||||
const { defaultStorageQuota, storageLabelClaim, storageQuotaClaim, roleClaim } = oauth;
|
||||
const storageLabel = this.getClaim(profile, {
|
||||
key: storageLabelClaim,
|
||||
default: '',
|
||||
isValid: (value: unknown): value is string => typeof value === 'string',
|
||||
});
|
||||
const storageQuota = this.getClaim(profile, {
|
||||
key: storageQuotaClaim,
|
||||
default: defaultStorageQuota,
|
||||
isValid: (value: unknown) => Number(value) >= 0,
|
||||
});
|
||||
const role = this.getClaim<'admin' | 'user'>(profile, {
|
||||
key: roleClaim,
|
||||
default: 'user',
|
||||
isValid: (value: unknown) => typeof value === 'string' && ['admin', 'user'].includes(value),
|
||||
});
|
||||
|
||||
return {
|
||||
name:
|
||||
profile.name ||
|
||||
`${profile.given_name || ''} ${profile.family_name || ''}`.trim() ||
|
||||
profile.preferred_username ||
|
||||
normalizedEmail,
|
||||
storageLabel: storageLabel || null,
|
||||
storageQuotaInGiB: storageQuota,
|
||||
isAdmin: role === 'admin',
|
||||
picture: profile.picture ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
private async applyOAuthProfileToUser(user: UserAdmin, record: { oauthSub: string; profile: OAuthLinkTokenProfile }) {
|
||||
const { profile } = record;
|
||||
const storageLabel = profile.storageLabel ? sanitize(profile.storageLabel.replaceAll('.', '')) : null;
|
||||
const updated = await this.userRepository.update(user.id, {
|
||||
oauthId: record.oauthSub,
|
||||
storageLabel,
|
||||
quotaSizeInBytes: profile.storageQuotaInGiB === null ? null : profile.storageQuotaInGiB * HumanReadableSize.GiB,
|
||||
isAdmin: profile.isAdmin,
|
||||
});
|
||||
if (!updated.profileImagePath && profile.picture) {
|
||||
await this.syncProfilePicture(updated, profile.picture);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
private async syncProfilePicture(user: UserAdmin, url: string) {
|
||||
try {
|
||||
const oldPath = user.profileImagePath;
|
||||
const { data } = await this.oauthRepository.getProfilePicture(url);
|
||||
|
||||
const { contentType, data } = await this.oauthRepository.getProfilePicture(url);
|
||||
const extensionWithDot = mimeTypes.toExtension(contentType || 'image/jpeg') ?? 'jpg';
|
||||
const profileImagePath = join(
|
||||
StorageCore.getFolderLocation(StorageFolder.Profile, user.id),
|
||||
`${this.cryptoRepository.randomUUID()}${extensionWithDot}`,
|
||||
const config = await this.getConfig({ withCache: true });
|
||||
const profileImagePath = await generateProfileImage(
|
||||
{ media: this.mediaRepository, crypto: this.cryptoRepository, storageCore: this.storageCore },
|
||||
config,
|
||||
user.id,
|
||||
Buffer.from(data),
|
||||
);
|
||||
|
||||
this.storageCore.ensureFolders(profileImagePath);
|
||||
await this.storageRepository.createFile(profileImagePath, Buffer.from(data));
|
||||
await this.userRepository.update(user.id, { profileImagePath, profileChangedAt: new Date() });
|
||||
|
||||
if (oldPath) {
|
||||
@@ -368,30 +520,11 @@ export class AuthService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async link(auth: AuthDto, dto: OAuthCallbackDto, headers: IncomingHttpHeaders): Promise<UserAdminResponseDto> {
|
||||
const expectedState = dto.state ?? this.getCookieOauthState(headers);
|
||||
if (!expectedState?.length) {
|
||||
throw new BadRequestException('OAuth state is missing');
|
||||
}
|
||||
|
||||
const codeVerifier = dto.codeVerifier ?? this.getCookieCodeVerifier(headers);
|
||||
if (!codeVerifier?.length) {
|
||||
throw new BadRequestException('OAuth code verifier is missing');
|
||||
}
|
||||
|
||||
const { oauth } = await this.getConfig({ withCache: false });
|
||||
const { sub: oauthId } = await this.oauthRepository.getProfile(oauth, dto.url, expectedState, codeVerifier);
|
||||
const duplicate = await this.userRepository.getByOAuthId(oauthId);
|
||||
if (duplicate && duplicate.id !== auth.user.id) {
|
||||
this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`);
|
||||
throw new BadRequestException('This OAuth account has already been linked to another user.');
|
||||
}
|
||||
|
||||
const user = await this.userRepository.update(auth.user.id, { oauthId });
|
||||
return mapUserAdmin(user);
|
||||
}
|
||||
|
||||
async unlink(auth: AuthDto): Promise<UserAdminResponseDto> {
|
||||
if (auth.session) {
|
||||
await this.sessionRepository.update(auth.session.id, { oauthSid: null });
|
||||
}
|
||||
|
||||
const user = await this.userRepository.update(auth.user.id, { oauthId: '' });
|
||||
return mapUserAdmin(user);
|
||||
}
|
||||
@@ -406,6 +539,10 @@ export class AuthService extends BaseService {
|
||||
return LOGIN_URL;
|
||||
}
|
||||
|
||||
if (config.oauth.endSessionEndpoint) {
|
||||
return config.oauth.endSessionEndpoint;
|
||||
}
|
||||
|
||||
return (await this.oauthRepository.getLogoutEndpoint(config.oauth)) || LOGIN_URL;
|
||||
}
|
||||
|
||||
@@ -433,6 +570,11 @@ export class AuthService extends BaseService {
|
||||
return cookies[ImmichCookie.OAuthCodeVerifier] || null;
|
||||
}
|
||||
|
||||
private getCookieOAuthLinkToken(headers: IncomingHttpHeaders): string | null {
|
||||
const cookies = parse(headers.cookie || '');
|
||||
return cookies[ImmichCookie.OAuthLinkToken] || null;
|
||||
}
|
||||
|
||||
async validateSharedLinkKey(key: string | string[]): Promise<AuthDto> {
|
||||
key = Array.isArray(key) ? key[0] : key;
|
||||
|
||||
@@ -548,7 +690,7 @@ export class AuthService extends BaseService {
|
||||
await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
|
||||
}
|
||||
|
||||
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
|
||||
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails, oauthSid?: string) {
|
||||
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||
const hashed = this.cryptoRepository.hashSha256(token);
|
||||
|
||||
@@ -558,6 +700,7 @@ export class AuthService extends BaseService {
|
||||
deviceType: loginDetails.deviceType,
|
||||
appVersion: loginDetails.appVersion,
|
||||
userId: user.id,
|
||||
oauthSid: oauthSid ?? null,
|
||||
});
|
||||
|
||||
return mapLoginResponse(user, token);
|
||||
|
||||
@@ -32,6 +32,7 @@ import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthLinkTokenRepository } from 'src/repositories/oauth-link-token.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
@@ -88,6 +89,7 @@ export const BASE_SERVICE_DEPENDENCIES = [
|
||||
MetadataRepository,
|
||||
MoveRepository,
|
||||
NotificationRepository,
|
||||
OAuthLinkTokenRepository,
|
||||
OAuthRepository,
|
||||
OcrRepository,
|
||||
PartnerRepository,
|
||||
@@ -146,6 +148,7 @@ export class BaseService {
|
||||
protected metadataRepository: MetadataRepository,
|
||||
protected moveRepository: MoveRepository,
|
||||
protected notificationRepository: NotificationRepository,
|
||||
protected oauthLinkTokenRepository: OAuthLinkTokenRepository,
|
||||
protected oauthRepository: OAuthRepository,
|
||||
protected ocrRepository: OcrRepository,
|
||||
protected partnerRepository: PartnerRepository,
|
||||
|
||||
@@ -142,6 +142,61 @@ describe(DownloadService.name, () => {
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_123.jpg', 'IMG_123+1.jpg');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ input: '../../../../tmp/pwn.jpg', expected: '........tmppwn.jpg' },
|
||||
{ input: String.raw`C:\temp\abs3.jpg`, expected: 'Ctempabs3.jpg' },
|
||||
{ input: 'a/../../b.jpg', expected: 'a....b.jpg' },
|
||||
{ input: String.raw`..\..\win1.jpg`, expected: '....win1.jpg' },
|
||||
{ input: '/etc/passwd', expected: 'etcpasswd' },
|
||||
{ input: '..', expected: 'unnamed' },
|
||||
{ input: '', expected: 'unnamed' },
|
||||
])('should sanitize unsafe originalFileName "$input" to "$expected"', async ({ input, expected }) => {
|
||||
const archiveMock = {
|
||||
addFile: vitest.fn(),
|
||||
finalize: vitest.fn(),
|
||||
stream: new Readable(),
|
||||
};
|
||||
const asset = AssetFactory.create({ originalFileName: input, originalPath: '/data/library/safe.jpg' });
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForOriginals.mockResolvedValue([asset]);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
});
|
||||
|
||||
expect(archiveMock.addFile).toHaveBeenCalledWith('/data/library/safe.jpg', expected);
|
||||
});
|
||||
|
||||
it('should dedupe sanitized duplicate unsafe filenames', async () => {
|
||||
const archiveMock = {
|
||||
addFile: vitest.fn(),
|
||||
finalize: vitest.fn(),
|
||||
stream: new Readable(),
|
||||
};
|
||||
const asset1 = AssetFactory.create({
|
||||
originalFileName: '../../../tmp/pwn.jpg',
|
||||
originalPath: '/data/library/a.jpg',
|
||||
});
|
||||
const asset2 = AssetFactory.create({
|
||||
originalFileName: '../../../tmp/pwn.jpg',
|
||||
originalPath: '/data/library/b.jpg',
|
||||
});
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id]));
|
||||
mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]);
|
||||
mocks.storage.createZipStream.mockReturnValue(archiveMock);
|
||||
|
||||
await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({
|
||||
stream: archiveMock.stream,
|
||||
});
|
||||
|
||||
expect(archiveMock.addFile).toHaveBeenCalledTimes(2);
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/a.jpg', '......tmppwn.jpg');
|
||||
expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/b.jpg', '......tmppwn+1.jpg');
|
||||
});
|
||||
|
||||
it('should resolve symlinks', async () => {
|
||||
const archiveMock = {
|
||||
addFile: vitest.fn(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { parse } from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { DownloadArchiveDto, DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
|
||||
@@ -95,11 +96,11 @@ export class DownloadService extends BaseService {
|
||||
|
||||
const { originalPath, editedPath, originalFileName } = asset;
|
||||
|
||||
let filename = originalFileName;
|
||||
let filename = sanitize(originalFileName) || 'unnamed';
|
||||
const count = paths[filename] || 0;
|
||||
paths[filename] = count + 1;
|
||||
if (count !== 0) {
|
||||
const parsedFilename = parse(originalFileName);
|
||||
const parsedFilename = parse(filename);
|
||||
filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ describe(ServerService.name, () => {
|
||||
reverseGeocoding: true,
|
||||
oauth: false,
|
||||
oauthAutoLaunch: false,
|
||||
oauthAutoRegister: true,
|
||||
ocr: true,
|
||||
passwordLogin: true,
|
||||
search: true,
|
||||
|
||||
@@ -102,6 +102,7 @@ export class ServerService extends BaseService {
|
||||
trash: trash.enabled,
|
||||
oauth: oauth.enabled,
|
||||
oauthAutoLaunch: oauth.autoLaunch,
|
||||
oauthAutoRegister: oauth.autoRegister,
|
||||
ocr: isOcrEnabled(machineLearning),
|
||||
passwordLogin: passwordLogin.enabled,
|
||||
configFile: !!configFile,
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('SessionService', () => {
|
||||
describe('handleCleanup', () => {
|
||||
it('should clean sessions', async () => {
|
||||
mocks.session.cleanup.mockResolvedValue([]);
|
||||
mocks.oauthLinkToken.cleanup.mockResolvedValue(0);
|
||||
await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.Success);
|
||||
});
|
||||
});
|
||||
@@ -46,11 +47,14 @@ describe('SessionService', () => {
|
||||
const currentSession = SessionFactory.create();
|
||||
const auth = AuthFactory.from().session(currentSession).build();
|
||||
|
||||
mocks.session.invalidate.mockResolvedValue();
|
||||
mocks.session.invalidateAll.mockResolvedValue();
|
||||
|
||||
await sut.deleteAll(auth);
|
||||
|
||||
expect(mocks.session.invalidate).toHaveBeenCalledWith({ userId: auth.user.id, excludeId: currentSession.id });
|
||||
expect(mocks.session.invalidateAll).toHaveBeenCalledWith({
|
||||
userId: auth.user.id,
|
||||
excludeId: currentSession.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ export class SessionService extends BaseService {
|
||||
|
||||
this.logger.log(`Deleted ${sessions.length} expired session tokens`);
|
||||
|
||||
const expiredLinkTokens = await this.oauthLinkTokenRepository.cleanup();
|
||||
if (expiredLinkTokens > 0) {
|
||||
this.logger.debug(`Deleted ${expiredLinkTokens} expired OAuth link tokens`);
|
||||
}
|
||||
|
||||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
@@ -73,7 +78,7 @@ export class SessionService extends BaseService {
|
||||
async deleteAll(auth: AuthDto): Promise<void> {
|
||||
const userId = auth.user.id;
|
||||
const currentSessionId = auth.session?.id;
|
||||
await this.sessionRepository.invalidate({ userId, excludeId: currentSessionId });
|
||||
await this.sessionRepository.invalidateAll({ userId, excludeId: currentSessionId });
|
||||
}
|
||||
|
||||
async lock(auth: AuthDto, id: string): Promise<void> {
|
||||
@@ -83,6 +88,6 @@ export class SessionService extends BaseService {
|
||||
|
||||
@OnEvent({ name: 'AuthChangePassword' })
|
||||
async onAuthChangePassword({ userId, currentSessionId }: ArgOf<'AuthChangePassword'>): Promise<void> {
|
||||
await this.sessionRepository.invalidate({ userId, excludeId: currentSessionId });
|
||||
await this.sessionRepository.invalidateAll({ userId, excludeId: currentSessionId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +138,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
defaultStorageQuota: null,
|
||||
enabled: false,
|
||||
issuerUrl: '',
|
||||
endSessionEndpoint: '',
|
||||
mobileOverrideEnabled: false,
|
||||
mobileRedirectUri: '',
|
||||
prompt: '',
|
||||
scope: 'openid email profile',
|
||||
signingAlgorithm: 'RS256',
|
||||
profileSigningAlgorithm: 'none',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { UserAdminService } from 'src/services/user-admin.service';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { describe } from 'vitest';
|
||||
@@ -165,6 +166,42 @@ describe(UserAdminService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOAuthReLinkToken', () => {
|
||||
it('should throw when OAuth is not enabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.disabled);
|
||||
await expect(sut.createOAuthReLinkToken(authStub.admin, userStub.user1.id)).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(mocks.oauthLinkToken.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when the target user is missing', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
mocks.user.get.mockResolvedValueOnce(void 0);
|
||||
await expect(sut.createOAuthReLinkToken(authStub.admin, 'missing')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.oauthLinkToken.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a token with null oauthSub and the target email', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
|
||||
mocks.oauthLinkToken.create.mockResolvedValue({} as any);
|
||||
|
||||
const result = await sut.createOAuthReLinkToken(authStub.admin, userStub.user1.id);
|
||||
|
||||
expect(mocks.oauthLinkToken.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
oauthSub: null,
|
||||
oauthSid: null,
|
||||
profile: null,
|
||||
email: userStub.user1.email,
|
||||
}),
|
||||
);
|
||||
expect(result.token).toEqual(expect.any(String));
|
||||
expect(result.expiresAt).toBeInstanceOf(Date);
|
||||
expect(result.expiresAt.getTime()).toBeGreaterThan(Date.now());
|
||||
});
|
||||
});
|
||||
|
||||
describe('restore', () => {
|
||||
it('should throw error if user could not be found', async () => {
|
||||
mocks.user.get.mockResolvedValue(void 0);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { AssetStatsDto, AssetStatsResponseDto, mapStats } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||
import {
|
||||
OAuthReLinkTokenResponseDto,
|
||||
UserAdminCreateDto,
|
||||
UserAdminDeleteDto,
|
||||
UserAdminResponseDto,
|
||||
@@ -137,6 +139,29 @@ export class UserAdminService extends BaseService {
|
||||
return mapPreferences(getPreferences(metadata));
|
||||
}
|
||||
|
||||
async createOAuthReLinkToken(auth: AuthDto, id: string): Promise<OAuthReLinkTokenResponseDto> {
|
||||
const { oauth } = await this.getConfig({ withCache: false });
|
||||
if (!oauth.enabled) {
|
||||
throw new BadRequestException('OAuth is not enabled');
|
||||
}
|
||||
|
||||
const user = await this.findOrFail(id, {});
|
||||
const plainToken = this.cryptoRepository.randomBytesAsText(32);
|
||||
const hashedToken = this.cryptoRepository.hashSha256(plainToken);
|
||||
const expiresAt = DateTime.now().plus({ hours: 24 }).toJSDate();
|
||||
await this.oauthLinkTokenRepository.create({
|
||||
token: hashedToken,
|
||||
oauthSub: null,
|
||||
oauthSid: null,
|
||||
email: user.email,
|
||||
profile: null,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
this.logger.log(`Admin ${auth.user.id} issued an OAuth re-link token for user ${user.id}`);
|
||||
return { token: plainToken, expiresAt };
|
||||
}
|
||||
|
||||
async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) {
|
||||
await this.findOrFail(id, { withDeleted: false });
|
||||
const metadata = await this.userRepository.getMetadata(id);
|
||||
|
||||
@@ -113,20 +113,34 @@ describe(UserService.name, () => {
|
||||
await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(InternalServerErrorException);
|
||||
});
|
||||
|
||||
it('should delete the previous profile image', async () => {
|
||||
it('should throw BadRequestException and clean up raw upload when thumbnail processing fails', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
const user = UserFactory.create({ profileImagePath: '/path/to/profile.jpg' });
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.media.generateThumbnail.mockRejectedValue(new Error('not an image'));
|
||||
|
||||
await expect(sut.createProfileImage(authStub.admin, file)).rejects.toThrowError(BadRequestException);
|
||||
|
||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.FileDelete, data: { files: [file.path] } }]]);
|
||||
});
|
||||
|
||||
it('should delete the raw upload and the previous profile image', async () => {
|
||||
const user = UserFactory.create({ profileImagePath: '/path/to/profile.jpg' });
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
const files = [user.profileImagePath];
|
||||
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
mocks.user.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
|
||||
await sut.createProfileImage(authStub.admin, file);
|
||||
|
||||
expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.FileDelete, data: { files } }]]);
|
||||
expect(mocks.job.queue.mock.calls).toEqual([
|
||||
[{ name: JobName.FileDelete, data: { files: [file.path, user.profileImagePath] } }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not delete the profile image if it has not been set', async () => {
|
||||
it('should delete only the raw upload if no previous profile image is set', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
|
||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||
@@ -134,7 +148,7 @@ describe(UserService.name, () => {
|
||||
|
||||
await sut.createProfileImage(authStub.admin, file);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queue.mock.calls).toEqual([[{ name: JobName.FileDelete, data: { files: [file.path] } }]]);
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -192,6 +206,19 @@ describe(UserService.name, () => {
|
||||
|
||||
expect(mocks.user.get).toHaveBeenCalledWith(user.id, {});
|
||||
});
|
||||
|
||||
it('should return the profile picture with the content-type matching the stored file', async () => {
|
||||
const user = UserFactory.create({ profileImagePath: '/path/to/profile.webp' });
|
||||
mocks.user.get.mockResolvedValue(user);
|
||||
|
||||
await expect(sut.getProfileImage(user.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/path/to/profile.webp',
|
||||
contentType: 'image/webp',
|
||||
cacheControl: CacheControl.None,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueUserDelete', () => {
|
||||
|
||||
@@ -16,7 +16,9 @@ import { UserTable } from 'src/schema/tables/user.table';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobOf, UserMetadataItem } from 'src/types';
|
||||
import { ImmichFileResponse } from 'src/utils/file';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
||||
import { generateProfileImage } from 'src/utils/profile-image';
|
||||
|
||||
@Injectable()
|
||||
export class UserService extends BaseService {
|
||||
@@ -91,16 +93,29 @@ export class UserService extends BaseService {
|
||||
}
|
||||
|
||||
async createProfileImage(auth: AuthDto, file: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
|
||||
const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false });
|
||||
const { profileImagePath: oldPath } = await this.findOrFail(auth.user.id, { withDeleted: false });
|
||||
|
||||
let profileImagePath: string;
|
||||
try {
|
||||
const config = await this.getConfig({ withCache: true });
|
||||
profileImagePath = await generateProfileImage(
|
||||
{ media: this.mediaRepository, crypto: this.cryptoRepository, storageCore: this.storageCore },
|
||||
config,
|
||||
auth.user.id,
|
||||
file.path,
|
||||
);
|
||||
} catch (error) {
|
||||
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [file.path] } });
|
||||
throw new BadRequestException('Unable to process profile image', { cause: error });
|
||||
}
|
||||
|
||||
const user = await this.userRepository.update(auth.user.id, {
|
||||
profileImagePath: file.path,
|
||||
profileImagePath,
|
||||
profileChangedAt: new Date(),
|
||||
});
|
||||
|
||||
if (oldpath !== '') {
|
||||
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: [oldpath] } });
|
||||
}
|
||||
const toDelete = [file.path, ...(oldPath ? [oldPath] : [])];
|
||||
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: toDelete } });
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
@@ -126,7 +141,7 @@ export class UserService extends BaseService {
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path: user.profileImagePath,
|
||||
contentType: 'image/jpeg',
|
||||
contentType: mimeTypes.lookup(user.profileImagePath),
|
||||
cacheControl: CacheControl.None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { join } from 'node:path';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { StorageFolder } from 'src/enum';
|
||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
|
||||
type Repos = {
|
||||
media: MediaRepository;
|
||||
crypto: CryptoRepository;
|
||||
storageCore: StorageCore;
|
||||
};
|
||||
|
||||
export const generateProfileImage = async (
|
||||
{ media, crypto, storageCore }: Repos,
|
||||
{ image }: SystemConfig,
|
||||
userId: string,
|
||||
input: string | Buffer,
|
||||
): Promise<string> => {
|
||||
const outputPath = join(
|
||||
StorageCore.getFolderLocation(StorageFolder.Profile, userId),
|
||||
`${crypto.randomUUID()}.${image.thumbnail.format}`,
|
||||
);
|
||||
storageCore.ensureFolders(outputPath);
|
||||
|
||||
await media.generateThumbnail(
|
||||
input,
|
||||
{
|
||||
colorspace: image.colorspace,
|
||||
format: image.thumbnail.format,
|
||||
quality: image.thumbnail.quality,
|
||||
progressive: image.thumbnail.progressive,
|
||||
size: image.thumbnail.size,
|
||||
processInvalidImages: false,
|
||||
},
|
||||
outputPath,
|
||||
);
|
||||
|
||||
return outputPath;
|
||||
};
|
||||
@@ -18,6 +18,7 @@ export const respondWithCookie = <T>(res: Response, body: T, { isSecure, values
|
||||
[ImmichCookie.MaintenanceToken]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() },
|
||||
[ImmichCookie.OAuthState]: defaults,
|
||||
[ImmichCookie.OAuthCodeVerifier]: defaults,
|
||||
[ImmichCookie.OAuthLinkToken]: { ...defaults, maxAge: Duration.fromObject({ minutes: 10 }).toMillis() },
|
||||
// no httpOnly so that the client can know the auth state
|
||||
[ImmichCookie.IsAuthenticated]: { ...defaults, httpOnly: false },
|
||||
[ImmichCookie.SharedLinkToken]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() },
|
||||
|
||||
@@ -32,6 +32,22 @@ export function IsIPRange(options?: IsIPRangeOptions) {
|
||||
.refine((arr) => arr.every((item) => isIPOrRange(item, options)), 'Must be an ip address or ip address range');
|
||||
}
|
||||
|
||||
/**
|
||||
* Like z.object().partial(), but rejects objects where every field is undefined.
|
||||
* Use for update/patch DTOs where at least one field must be provided.
|
||||
*
|
||||
* @example
|
||||
* nonEmptyPartial({ name: z.string(), bio: z.string() }).meta({ id: 'UpdateDto' });
|
||||
*/
|
||||
export function nonEmptyPartial<T extends z.ZodRawShape>(shape: T) {
|
||||
return z
|
||||
.object(shape)
|
||||
.partial()
|
||||
.refine((data) => Object.values(data as Record<string, unknown>).some((value) => value !== undefined), {
|
||||
message: 'At least one field must be provided',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Zod schema that validates sibling-exclusion for object schemas.
|
||||
* Validation passes when the target property is missing, or when none of the sibling properties are present.
|
||||
|
||||
@@ -25,6 +25,7 @@ export class SessionFactory {
|
||||
updateId: newUuidV7(),
|
||||
updatedAt: newDate(),
|
||||
userId: newUuid(),
|
||||
oauthSid: newUuid(),
|
||||
...dto,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ describe(AuthService.name, () => {
|
||||
const { user } = await ctx.newUser({ password: passwordHashed });
|
||||
const dto = { email: user.email, password: 'wrong-password' };
|
||||
|
||||
await expect(sut.login(dto, mediumFactory.loginDetails())).rejects.toThrow('Incorrect email or password');
|
||||
await expect(sut.login(dto, mediumFactory.loginDetails(), {})).rejects.toThrow('Incorrect email or password');
|
||||
});
|
||||
|
||||
it('should accept a correct password and return a login response', async () => {
|
||||
@@ -87,7 +87,7 @@ describe(AuthService.name, () => {
|
||||
const { user } = await ctx.newUser({ password: passwordHashed });
|
||||
const dto = { email: user.email, password };
|
||||
|
||||
await expect(sut.login(dto, mediumFactory.loginDetails())).resolves.toEqual({
|
||||
await expect(sut.login(dto, mediumFactory.loginDetails(), {})).resolves.toEqual({
|
||||
accessToken: expect.any(String),
|
||||
isAdmin: user.isAdmin,
|
||||
isOnboarded: false,
|
||||
@@ -147,7 +147,7 @@ describe(AuthService.name, () => {
|
||||
expect((response as any).password).not.toBeDefined();
|
||||
|
||||
await expect(
|
||||
sut.login({ email: user.email, password: dto.newPassword }, mediumFactory.loginDetails()),
|
||||
sut.login({ email: user.email, password: dto.newPassword }, mediumFactory.loginDetails(), {}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||
import { MoveRepository } from 'src/repositories/move.repository';
|
||||
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||
import { OAuthLinkTokenRepository } from 'src/repositories/oauth-link-token.repository';
|
||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
@@ -239,6 +240,7 @@ export type ServiceOverrides = {
|
||||
metadata: MetadataRepository;
|
||||
move: MoveRepository;
|
||||
notification: NotificationRepository;
|
||||
oauthLinkToken: OAuthLinkTokenRepository;
|
||||
ocr: OcrRepository;
|
||||
oauth: OAuthRepository;
|
||||
partner: PartnerRepository;
|
||||
@@ -321,6 +323,7 @@ export const getMocks = () => {
|
||||
move: automock(MoveRepository, { strict: false }),
|
||||
notification: automock(NotificationRepository),
|
||||
ocr: automock(OcrRepository, { strict: false }),
|
||||
oauthLinkToken: automock(OAuthLinkTokenRepository),
|
||||
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
||||
partner: automock(PartnerRepository, { strict: false }),
|
||||
person: automock(PersonRepository, { strict: false }),
|
||||
@@ -387,6 +390,7 @@ export const newTestService = <T extends BaseService>(
|
||||
overrides.metadata || (mocks.metadata as As<MetadataRepository>),
|
||||
overrides.move || (mocks.move as As<MoveRepository>),
|
||||
overrides.notification || (mocks.notification as As<NotificationRepository>),
|
||||
overrides.oauthLinkToken || (mocks.oauthLinkToken as As<OAuthLinkTokenRepository>),
|
||||
overrides.oauth || (mocks.oauth as As<OAuthRepository>),
|
||||
overrides.ocr || (mocks.ocr as As<OcrRepository>),
|
||||
overrides.partner || (mocks.partner as As<PartnerRepository>),
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"@immich/ui": "^0.76.0",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0",
|
||||
"@photo-sphere-viewer/markers-plugin": "^5.14.0",
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { oauth } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { Button, LoadingSpinner, toastManager } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
if (oauth.isCallback(globalThis.location)) {
|
||||
try {
|
||||
loading = true;
|
||||
const response = await oauth.link(globalThis.location);
|
||||
authManager.setUser(response);
|
||||
toastManager.primary($t('linked_oauth_account'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_link_oauth_account'));
|
||||
} finally {
|
||||
await goto('?open=oauth');
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
});
|
||||
|
||||
const handleUnlink = async () => {
|
||||
try {
|
||||
const response = await oauth.unlink();
|
||||
authManager.setUser(response);
|
||||
toastManager.primary($t('unlinked_oauth_account'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_unlink_account'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="sm:ms-8 flex justify-end">
|
||||
{#if loading}
|
||||
<div class="flex place-content-center place-items-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else if featureFlagsManager.value.oauth}
|
||||
{#if authManager.user.oauthId}
|
||||
<Button shape="round" size="small" onclick={() => handleUnlink()}>{$t('unlink_oauth')}</Button>
|
||||
{:else}
|
||||
<Button shape="round" size="small" onclick={() => oauth.authorize(globalThis.location)}
|
||||
>{$t('link_to_oauth')}</Button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -25,7 +25,7 @@ export async function loadFromTimeBuckets(
|
||||
{ signal },
|
||||
);
|
||||
|
||||
if (!bucketResponse) {
|
||||
if (!bucketResponse || signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function loadFromTimeBuckets(
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
if (!albumAssets) {
|
||||
if (!albumAssets || signal.aborted) {
|
||||
return;
|
||||
}
|
||||
for (const id of albumAssets.id) {
|
||||
|
||||
@@ -51,6 +51,7 @@ export const Docs = {
|
||||
export const Route = {
|
||||
// auth
|
||||
login: (params?: { continue?: string; autoLaunch?: 0 | 1 }) => '/auth/login' + asQueryString(params),
|
||||
authLink: (params?: { email?: string }) => '/auth/link' + asQueryString(params),
|
||||
logout: (params?: { continue?: string }) => '/auth/logout' + asQueryString(params),
|
||||
register: () => '/auth/register',
|
||||
changePassword: () => '/auth/change-password',
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
getBaseUrl,
|
||||
getPeopleThumbnailPath,
|
||||
getUserProfileImagePath,
|
||||
linkOAuthAccount,
|
||||
startOAuth,
|
||||
unlinkOAuthAccount,
|
||||
type AssetResponseDto,
|
||||
@@ -293,9 +292,6 @@ export const oauth = {
|
||||
login: (location: Location) => {
|
||||
return finishOAuth({ oAuthCallbackDto: { url: location.href } });
|
||||
},
|
||||
link: (location: Location) => {
|
||||
return linkOAuthAccount({ oAuthCallbackDto: { url: location.href } });
|
||||
},
|
||||
unlink: () => {
|
||||
return unlinkOAuthAccount();
|
||||
},
|
||||
|
||||
@@ -20,9 +20,6 @@ describe('fileUploader error handling', () => {
|
||||
vi.spyOn(uploadManager, 'getExtensions').mockReturnValue(['.jpg']);
|
||||
uploadAssetsStore.reset();
|
||||
authManager.reset();
|
||||
|
||||
// Stub out crypto to avoid that branch
|
||||
vi.stubGlobal('crypto', undefined);
|
||||
});
|
||||
|
||||
for (const [name, mockUser] of [
|
||||
|
||||
@@ -127,6 +127,30 @@ function getDeviceAssetId(asset: File) {
|
||||
return 'web' + '-' + asset.name + '-' + asset.lastModified;
|
||||
}
|
||||
|
||||
function hashFile(file: File): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const worker = new Worker(new URL('$lib/workers/hash-file.ts', import.meta.url), { type: 'module' });
|
||||
|
||||
worker.addEventListener('message', ({ data }: MessageEvent<{ result?: string; error?: string }>) => {
|
||||
worker.terminate();
|
||||
|
||||
if (data.error) {
|
||||
reject(new Error(data.error));
|
||||
} else {
|
||||
resolve(data.result!);
|
||||
}
|
||||
});
|
||||
|
||||
worker.addEventListener('error', (event) => {
|
||||
worker.terminate();
|
||||
|
||||
reject(new Error(event.message));
|
||||
});
|
||||
|
||||
worker.postMessage(file);
|
||||
});
|
||||
}
|
||||
|
||||
type FileUploaderParams = {
|
||||
assetFile: File;
|
||||
albumId?: string;
|
||||
@@ -165,15 +189,11 @@ async function fileUploader({
|
||||
}
|
||||
|
||||
let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined;
|
||||
if (crypto?.subtle?.digest && !authManager.isSharedLink) {
|
||||
if (!authManager.isSharedLink) {
|
||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
|
||||
await tick();
|
||||
try {
|
||||
const bytes = await assetFile.arrayBuffer();
|
||||
const hash = await crypto.subtle.digest('SHA-1', bytes);
|
||||
const checksum = Array.from(new Uint8Array(hash))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
const checksum = await hashFile(assetFile);
|
||||
|
||||
const {
|
||||
results: [checkUploadResult],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user