Merge branch 'main' into feature/readonly-sharing

# Conflicts:
#	mobile/openapi/.openapi-generator/FILES
#	mobile/openapi/README.md
#	mobile/openapi/lib/api.dart
#	mobile/openapi/lib/api_client.dart
This commit is contained in:
mgabor
2024-04-19 16:24:54 +02:00
81 changed files with 1377 additions and 1062 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:7e227295e96f5b00aa79555ae166f50610940d888fc2e321cf36304cbd17d7d6 as core FROM node:20-alpine3.19@sha256:ec0c413b1d84f3f7f67ec986ba885930c57b5318d2eb3abc6960ee05d4f2eb28 as core
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+1 -1
View File
@@ -97,7 +97,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
database: database:
container_name: immich_postgres container_name: immich_postgres
+1 -1
View File
@@ -54,7 +54,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
restart: always restart: always
database: database:
+1 -1
View File
@@ -58,7 +58,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
restart: always restart: always
database: database:
+2 -2
View File
@@ -52,8 +52,8 @@ Before enabling OAuth in Immich, a new client application needs to be configured
Hostname Hostname
- `https://immich.example.com/auth/login`) - `https://immich.example.com/auth/login`
- `https://immich.example.com/user-settings`) - `https://immich.example.com/user-settings`
## Enable OAuth ## Enable OAuth
+1 -1
View File
@@ -1,4 +1,4 @@
Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level. Immich allows the admin user to set the uploaded filename pattern at the directory and filename level as well as the [storage label for a user](/docs/administration/user-management/#set-storage-label-for-user).
:::note new version :::note new version
On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further). On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further).
+1 -1
View File
@@ -36,7 +36,7 @@ services:
<<: *server-common <<: *server-common
redis: redis:
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70 image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
database: database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
+4 -65
View File
@@ -1,7 +1,7 @@
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk'; import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto, uuidDto } from 'src/fixtures'; import { loginDto, signupDto } from 'src/fixtures';
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses'; import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
@@ -118,67 +118,6 @@ describe('/auth/*', () => {
}); });
}); });
describe('GET /auth/devices', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/auth/devices');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get a list of authorized devices', async () => {
const { status, body } = await request(app)
.get('/auth/devices')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([deviceDto.current]);
});
});
describe('DELETE /auth/devices', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/auth/devices`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should logout all devices (except the current one)', async () => {
for (let i = 0; i < 5; i++) {
await login({ loginCredentialDto: loginDto.admin });
}
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
});
it('should throw an error for a non-existent device id', async () => {
const { status, body } = await request(app)
.delete(`/auth/devices/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
});
it('should logout a device', async () => {
const [device] = await getAuthDevices({
headers: asBearerAuth(admin.accessToken),
});
const { status } = await request(app)
.delete(`/auth/devices/${device.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const response = await request(app)
.post('/auth/validateToken')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response.body).toEqual(errorDto.invalidToken);
expect(response.status).toBe(401);
});
});
describe('POST /auth/validateToken', () => { describe('POST /auth/validateToken', () => {
it('should reject an invalid token', async () => { it('should reject an invalid token', async () => {
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123'); const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
+75
View File
@@ -0,0 +1,75 @@
import { LoginResponseDto, getSessions, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
import { deviceDto, errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeEach, describe, expect, it } from 'vitest';
describe('/sessions', () => {
let admin: LoginResponseDto;
beforeEach(async () => {
await utils.resetDatabase();
await signUpAdmin({ signUpDto: signupDto.admin });
admin = await login({ loginCredentialDto: loginDto.admin });
});
describe('GET /sessions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/sessions');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get a list of authorized devices', async () => {
const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([deviceDto.current]);
});
});
describe('DELETE /sessions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/sessions`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should logout all devices (except the current one)', async () => {
for (let i = 0; i < 5; i++) {
await login({ loginCredentialDto: loginDto.admin });
}
await expect(getSessions({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
const { status } = await request(app).delete(`/sessions`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await expect(getSessions({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
});
it('should throw an error for a non-existent device id', async () => {
const { status, body } = await request(app)
.delete(`/sessions/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
});
it('should logout a device', async () => {
const [device] = await getSessions({
headers: asBearerAuth(admin.accessToken),
});
const { status } = await request(app)
.delete(`/sessions/${device.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const response = await request(app)
.post('/auth/validateToken')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response.body).toEqual(errorDto.invalidToken);
expect(response.status).toBe(401);
});
});
});
@@ -0,0 +1,19 @@
import { immichAdmin, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
describe(`immich-admin`, () => {
beforeAll(async () => {
await utils.resetDatabase();
await utils.adminSetup();
});
describe('list-users', () => {
it('should list the admin user', async () => {
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']);
expect(exitCode).toBe(0);
expect(stderr).toBe('');
expect(stdout).toContain("email: 'admin@immich.cloud'");
expect(stdout).toContain("name: 'Immich Admin'");
});
});
});
+11 -9
View File
@@ -44,7 +44,7 @@ import { loginDto, signupDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators'; import { makeRandomImage } from 'src/generators';
import request from 'supertest'; import request from 'supertest';
type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete'; type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete';
type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number }; type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number };
type AdminSetupOptions = { onboarding?: boolean }; type AdminSetupOptions = { onboarding?: boolean };
@@ -60,13 +60,15 @@ export const testAssetDirInternal = '/data/assets';
export const tempDir = tmpdir(); export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` }); export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = async (args: string[]) => { export const immichCli = (args: string[]) =>
let _resolve: (value: CliResponse) => void; executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]);
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve)); export const immichAdmin = (args: string[]) =>
const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]; executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
const child = spawn('node', _args, {
stdio: 'pipe', const executeCommand = (command: string, args: string[]) => {
}); let _resolve: (value: CommandResponse) => void;
const deferred = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const child = spawn(command, args, { stdio: 'pipe' });
let stdout = ''; let stdout = '';
let stderr = ''; let stderr = '';
@@ -139,7 +141,7 @@ export const utils = {
'asset_faces', 'asset_faces',
'activity', 'activity',
'api_keys', 'api_keys',
'user_token', 'sessions',
'users', 'users',
'system_metadata', 'system_metadata',
'system_config', 'system_config',
+1 -1
View File
@@ -10,7 +10,7 @@ try {
export default defineConfig({ export default defineConfig({
test: { test: {
include: ['src/{api,cli}/specs/*.e2e-spec.ts'], include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
globalSetup, globalSetup,
testTimeout: 15_000, testTimeout: 15_000,
poolOptions: { poolOptions: {
+6 -5
View File
@@ -1150,22 +1150,23 @@ test = ["objgraph", "psutil"]
[[package]] [[package]]
name = "gunicorn" name = "gunicorn"
version = "21.2.0" version = "22.0.0"
description = "WSGI HTTP Server for UNIX" description = "WSGI HTTP Server for UNIX"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.7"
files = [ files = [
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"},
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"},
] ]
[package.dependencies] [package.dependencies]
packaging = "*" packaging = "*"
[package.extras] [package.extras]
eventlet = ["eventlet (>=0.24.1)"] eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
gevent = ["gevent (>=1.4.0)"] gevent = ["gevent (>=1.4.0)"]
setproctitle = ["setproctitle"] setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"] tornado = ["tornado (>=0.2)"]
[[package]] [[package]]
Binary file not shown.
@@ -39,27 +39,24 @@ class ImmichAppBarDialog extends HookConsumerWidget {
); );
buildTopRow() { buildTopRow() {
return Row( return Stack(
children: [ children: [
InkWell( Align(
onTap: () => context.pop(), alignment: Alignment.topLeft,
child: const Icon( child: InkWell(
Icons.close, onTap: () => context.pop(),
size: 20, child: const Icon(
Icons.close,
size: 20,
),
), ),
), ),
Expanded( Center(
child: Align( child: Image.asset(
alignment: Alignment.center, context.isDarkTheme
child: Text( ? 'assets/immich-text-dark.png'
'IMMICH', : 'assets/immich-text-light.png',
style: TextStyle( height: 16,
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
color: context.primaryColor,
fontSize: 16,
),
),
), ),
), ),
], ],
+6 -12
View File
@@ -16,8 +16,6 @@ doc/AddUsersDto.md
doc/AlbumApi.md doc/AlbumApi.md
doc/AlbumCountResponseDto.md doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md doc/AlbumResponseDto.md
doc/AlbumUserResponseDto.md
doc/AlbumUserRole.md
doc/AllJobStatusResponseDto.md doc/AllJobStatusResponseDto.md
doc/AssetApi.md doc/AssetApi.md
doc/AssetBulkDeleteDto.md doc/AssetBulkDeleteDto.md
@@ -43,7 +41,6 @@ doc/AssetTypeEnum.md
doc/AudioCodec.md doc/AudioCodec.md
doc/AuditApi.md doc/AuditApi.md
doc/AuditDeletesResponseDto.md doc/AuditDeletesResponseDto.md
doc/AuthDeviceResponseDto.md
doc/AuthenticationApi.md doc/AuthenticationApi.md
doc/BulkIdResponseDto.md doc/BulkIdResponseDto.md
doc/BulkIdsDto.md doc/BulkIdsDto.md
@@ -144,6 +141,8 @@ doc/ServerPingResponse.md
doc/ServerStatsResponseDto.md doc/ServerStatsResponseDto.md
doc/ServerThemeDto.md doc/ServerThemeDto.md
doc/ServerVersionResponseDto.md doc/ServerVersionResponseDto.md
doc/SessionResponseDto.md
doc/SessionsApi.md
doc/SharedLinkApi.md doc/SharedLinkApi.md
doc/SharedLinkCreateDto.md doc/SharedLinkCreateDto.md
doc/SharedLinkEditDto.md doc/SharedLinkEditDto.md
@@ -186,7 +185,6 @@ doc/TranscodeHWAccel.md
doc/TranscodePolicy.md doc/TranscodePolicy.md
doc/TrashApi.md doc/TrashApi.md
doc/UpdateAlbumDto.md doc/UpdateAlbumDto.md
doc/UpdateAlbumUserDto.md
doc/UpdateAssetDto.md doc/UpdateAssetDto.md
doc/UpdateLibraryDto.md doc/UpdateLibraryDto.md
doc/UpdatePartnerDto.md doc/UpdatePartnerDto.md
@@ -222,6 +220,7 @@ lib/api/partner_api.dart
lib/api/person_api.dart lib/api/person_api.dart
lib/api/search_api.dart lib/api/search_api.dart
lib/api/server_info_api.dart lib/api/server_info_api.dart
lib/api/sessions_api.dart
lib/api/shared_link_api.dart lib/api/shared_link_api.dart
lib/api/sync_api.dart lib/api/sync_api.dart
lib/api/system_config_api.dart lib/api/system_config_api.dart
@@ -243,8 +242,6 @@ lib/model/activity_statistics_response_dto.dart
lib/model/add_users_dto.dart lib/model/add_users_dto.dart
lib/model/album_count_response_dto.dart lib/model/album_count_response_dto.dart
lib/model/album_response_dto.dart lib/model/album_response_dto.dart
lib/model/album_user_response_dto.dart
lib/model/album_user_role.dart
lib/model/all_job_status_response_dto.dart lib/model/all_job_status_response_dto.dart
lib/model/api_key_create_dto.dart lib/model/api_key_create_dto.dart
lib/model/api_key_create_response_dto.dart lib/model/api_key_create_response_dto.dart
@@ -272,7 +269,6 @@ lib/model/asset_stats_response_dto.dart
lib/model/asset_type_enum.dart lib/model/asset_type_enum.dart
lib/model/audio_codec.dart lib/model/audio_codec.dart
lib/model/audit_deletes_response_dto.dart lib/model/audit_deletes_response_dto.dart
lib/model/auth_device_response_dto.dart
lib/model/bulk_id_response_dto.dart lib/model/bulk_id_response_dto.dart
lib/model/bulk_ids_dto.dart lib/model/bulk_ids_dto.dart
lib/model/change_password_dto.dart lib/model/change_password_dto.dart
@@ -362,6 +358,7 @@ lib/model/server_ping_response.dart
lib/model/server_stats_response_dto.dart lib/model/server_stats_response_dto.dart
lib/model/server_theme_dto.dart lib/model/server_theme_dto.dart
lib/model/server_version_response_dto.dart lib/model/server_version_response_dto.dart
lib/model/session_response_dto.dart
lib/model/shared_link_create_dto.dart lib/model/shared_link_create_dto.dart
lib/model/shared_link_edit_dto.dart lib/model/shared_link_edit_dto.dart
lib/model/shared_link_response_dto.dart lib/model/shared_link_response_dto.dart
@@ -398,7 +395,6 @@ lib/model/tone_mapping.dart
lib/model/transcode_hw_accel.dart lib/model/transcode_hw_accel.dart
lib/model/transcode_policy.dart lib/model/transcode_policy.dart
lib/model/update_album_dto.dart lib/model/update_album_dto.dart
lib/model/update_album_user_dto.dart
lib/model/update_asset_dto.dart lib/model/update_asset_dto.dart
lib/model/update_library_dto.dart lib/model/update_library_dto.dart
lib/model/update_partner_dto.dart lib/model/update_partner_dto.dart
@@ -424,8 +420,6 @@ test/add_users_dto_test.dart
test/album_api_test.dart test/album_api_test.dart
test/album_count_response_dto_test.dart test/album_count_response_dto_test.dart
test/album_response_dto_test.dart test/album_response_dto_test.dart
test/album_user_response_dto_test.dart
test/album_user_role_test.dart
test/all_job_status_response_dto_test.dart test/all_job_status_response_dto_test.dart
test/api_key_api_test.dart test/api_key_api_test.dart
test/api_key_create_dto_test.dart test/api_key_create_dto_test.dart
@@ -456,7 +450,6 @@ test/asset_type_enum_test.dart
test/audio_codec_test.dart test/audio_codec_test.dart
test/audit_api_test.dart test/audit_api_test.dart
test/audit_deletes_response_dto_test.dart test/audit_deletes_response_dto_test.dart
test/auth_device_response_dto_test.dart
test/authentication_api_test.dart test/authentication_api_test.dart
test/bulk_id_response_dto_test.dart test/bulk_id_response_dto_test.dart
test/bulk_ids_dto_test.dart test/bulk_ids_dto_test.dart
@@ -557,6 +550,8 @@ test/server_ping_response_test.dart
test/server_stats_response_dto_test.dart test/server_stats_response_dto_test.dart
test/server_theme_dto_test.dart test/server_theme_dto_test.dart
test/server_version_response_dto_test.dart test/server_version_response_dto_test.dart
test/session_response_dto_test.dart
test/sessions_api_test.dart
test/shared_link_api_test.dart test/shared_link_api_test.dart
test/shared_link_create_dto_test.dart test/shared_link_create_dto_test.dart
test/shared_link_edit_dto_test.dart test/shared_link_edit_dto_test.dart
@@ -599,7 +594,6 @@ test/transcode_hw_accel_test.dart
test/transcode_policy_test.dart test/transcode_policy_test.dart
test/trash_api_test.dart test/trash_api_test.dart
test/update_album_dto_test.dart test/update_album_dto_test.dart
test/update_album_user_dto_test.dart
test/update_asset_dto_test.dart test/update_asset_dto_test.dart
test/update_library_dto_test.dart test/update_library_dto_test.dart
test/update_partner_dto_test.dart test/update_partner_dto_test.dart
+4 -8
View File
@@ -91,7 +91,6 @@ Class | Method | HTTP request | Description
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets | *AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} | *AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} |
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} | *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{id} |
*AlbumApi* | [**updateAlbumUser**](doc//AlbumApi.md#updatealbumuser) | **PUT** /album/{id}/permission/{userId} |
*AssetApi* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check | *AssetApi* | [**checkBulkUpload**](doc//AssetApi.md#checkbulkupload) | **POST** /asset/bulk-upload-check |
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
*AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset | *AssetApi* | [**deleteAssets**](doc//AssetApi.md#deleteassets) | **DELETE** /asset |
@@ -118,11 +117,8 @@ Class | Method | HTTP request | Description
*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report | *AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report |
*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum | *AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
*AuthenticationApi* | [**getAuthDevices**](doc//AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
*AuthenticationApi* | [**logoutAuthDevice**](doc//AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} |
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
@@ -184,6 +180,9 @@ Class | Method | HTTP request | Description
*ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme | *ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping | *ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
*ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding | *ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding |
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
*SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets | *SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets |
*SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link | *SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link | *SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
@@ -236,8 +235,6 @@ Class | Method | HTTP request | Description
- [AddUsersDto](doc//AddUsersDto.md) - [AddUsersDto](doc//AddUsersDto.md)
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AlbumUserResponseDto](doc//AlbumUserResponseDto.md)
- [AlbumUserRole](doc//AlbumUserRole.md)
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
- [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
- [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
@@ -261,7 +258,6 @@ Class | Method | HTTP request | Description
- [AssetTypeEnum](doc//AssetTypeEnum.md) - [AssetTypeEnum](doc//AssetTypeEnum.md)
- [AudioCodec](doc//AudioCodec.md) - [AudioCodec](doc//AudioCodec.md)
- [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md) - [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md)
- [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md)
- [BulkIdResponseDto](doc//BulkIdResponseDto.md) - [BulkIdResponseDto](doc//BulkIdResponseDto.md)
- [BulkIdsDto](doc//BulkIdsDto.md) - [BulkIdsDto](doc//BulkIdsDto.md)
- [CLIPConfig](doc//CLIPConfig.md) - [CLIPConfig](doc//CLIPConfig.md)
@@ -351,6 +347,7 @@ Class | Method | HTTP request | Description
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
- [ServerThemeDto](doc//ServerThemeDto.md) - [ServerThemeDto](doc//ServerThemeDto.md)
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
- [SessionResponseDto](doc//SessionResponseDto.md)
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
- [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md)
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
@@ -387,7 +384,6 @@ Class | Method | HTTP request | Description
- [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md) - [TranscodePolicy](doc//TranscodePolicy.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md) - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateLibraryDto](doc//UpdateLibraryDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md)
- [UpdatePartnerDto](doc//UpdatePartnerDto.md) - [UpdatePartnerDto](doc//UpdatePartnerDto.md)
-158
View File
@@ -10,11 +10,8 @@ All URIs are relative to */api*
Method | HTTP request | Description Method | HTTP request | Description
------------- | ------------- | ------------- ------------- | ------------- | -------------
[**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password | [**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
[**getAuthDevices**](AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
[**login**](AuthenticationApi.md#login) | **POST** /auth/login | [**login**](AuthenticationApi.md#login) | **POST** /auth/login |
[**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout | [**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout |
[**logoutAuthDevice**](AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} |
[**logoutAuthDevices**](AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
[**signUpAdmin**](AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | [**signUpAdmin**](AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
[**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | [**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
@@ -74,57 +71,6 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getAuthDevices**
> List<AuthDeviceResponseDto> getAuthDevices()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuthenticationApi();
try {
final result = api_instance.getAuthDevices();
print(result);
} catch (e) {
print('Exception when calling AuthenticationApi->getAuthDevices: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**List<AuthDeviceResponseDto>**](AuthDeviceResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **login** # **login**
> LoginResponseDto login(loginCredentialDto) > LoginResponseDto login(loginCredentialDto)
@@ -217,110 +163,6 @@ This endpoint does not need any parameter.
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **logoutAuthDevice**
> logoutAuthDevice(id)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuthenticationApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
api_instance.logoutAuthDevice(id);
} catch (e) {
print('Exception when calling AuthenticationApi->logoutAuthDevice: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **logoutAuthDevices**
> logoutAuthDevices()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AuthenticationApi();
try {
api_instance.logoutAuthDevices();
} catch (e) {
print('Exception when calling AuthenticationApi->logoutAuthDevices: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **signUpAdmin** # **signUpAdmin**
> UserResponseDto signUpAdmin(signUpDto) > UserResponseDto signUpAdmin(signUpDto)
@@ -1,4 +1,4 @@
# openapi.model.AuthDeviceResponseDto # openapi.model.SessionResponseDto
## Load the model package ## Load the model package
```dart ```dart
+171
View File
@@ -0,0 +1,171 @@
# openapi.api.SessionsApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**deleteAllSessions**](SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
[**deleteSession**](SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
[**getSessions**](SessionsApi.md#getsessions) | **GET** /sessions |
# **deleteAllSessions**
> deleteAllSessions()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SessionsApi();
try {
api_instance.deleteAllSessions();
} catch (e) {
print('Exception when calling SessionsApi->deleteAllSessions: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **deleteSession**
> deleteSession(id)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SessionsApi();
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
try {
api_instance.deleteSession(id);
} catch (e) {
print('Exception when calling SessionsApi->deleteSession: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
### Return type
void (empty response body)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: Not defined
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getSessions**
> List<SessionResponseDto> getSessions()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SessionsApi();
try {
final result = api_instance.getSessions();
print(result);
} catch (e) {
print('Exception when calling SessionsApi->getSessions: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**List<SessionResponseDto>**](SessionResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+2 -4
View File
@@ -45,6 +45,7 @@ part 'api/partner_api.dart';
part 'api/person_api.dart'; part 'api/person_api.dart';
part 'api/search_api.dart'; part 'api/search_api.dart';
part 'api/server_info_api.dart'; part 'api/server_info_api.dart';
part 'api/sessions_api.dart';
part 'api/shared_link_api.dart'; part 'api/shared_link_api.dart';
part 'api/sync_api.dart'; part 'api/sync_api.dart';
part 'api/system_config_api.dart'; part 'api/system_config_api.dart';
@@ -63,8 +64,6 @@ part 'model/activity_statistics_response_dto.dart';
part 'model/add_users_dto.dart'; part 'model/add_users_dto.dart';
part 'model/album_count_response_dto.dart'; part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart'; part 'model/album_response_dto.dart';
part 'model/album_user_response_dto.dart';
part 'model/album_user_role.dart';
part 'model/all_job_status_response_dto.dart'; part 'model/all_job_status_response_dto.dart';
part 'model/asset_bulk_delete_dto.dart'; part 'model/asset_bulk_delete_dto.dart';
part 'model/asset_bulk_update_dto.dart'; part 'model/asset_bulk_update_dto.dart';
@@ -88,7 +87,6 @@ part 'model/asset_stats_response_dto.dart';
part 'model/asset_type_enum.dart'; part 'model/asset_type_enum.dart';
part 'model/audio_codec.dart'; part 'model/audio_codec.dart';
part 'model/audit_deletes_response_dto.dart'; part 'model/audit_deletes_response_dto.dart';
part 'model/auth_device_response_dto.dart';
part 'model/bulk_id_response_dto.dart'; part 'model/bulk_id_response_dto.dart';
part 'model/bulk_ids_dto.dart'; part 'model/bulk_ids_dto.dart';
part 'model/clip_config.dart'; part 'model/clip_config.dart';
@@ -178,6 +176,7 @@ part 'model/server_ping_response.dart';
part 'model/server_stats_response_dto.dart'; part 'model/server_stats_response_dto.dart';
part 'model/server_theme_dto.dart'; part 'model/server_theme_dto.dart';
part 'model/server_version_response_dto.dart'; part 'model/server_version_response_dto.dart';
part 'model/session_response_dto.dart';
part 'model/shared_link_create_dto.dart'; part 'model/shared_link_create_dto.dart';
part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_edit_dto.dart';
part 'model/shared_link_response_dto.dart'; part 'model/shared_link_response_dto.dart';
@@ -214,7 +213,6 @@ part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart'; part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart'; part 'model/transcode_policy.dart';
part 'model/update_album_dto.dart'; part 'model/update_album_dto.dart';
part 'model/update_album_user_dto.dart';
part 'model/update_asset_dto.dart'; part 'model/update_asset_dto.dart';
part 'model/update_library_dto.dart'; part 'model/update_library_dto.dart';
part 'model/update_partner_dto.dart'; part 'model/update_partner_dto.dart';
-117
View File
@@ -63,50 +63,6 @@ class AuthenticationApi {
return null; return null;
} }
/// Performs an HTTP 'GET /auth/devices' operation and returns the [Response].
Future<Response> getAuthDevicesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/auth/devices';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<List<AuthDeviceResponseDto>?> getAuthDevices() async {
final response = await getAuthDevicesWithHttpInfo();
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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AuthDeviceResponseDto>') as List)
.cast<AuthDeviceResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'POST /auth/login' operation and returns the [Response]. /// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
@@ -195,79 +151,6 @@ class AuthenticationApi {
return null; return null;
} }
/// Performs an HTTP 'DELETE /auth/devices/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> logoutAuthDeviceWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/auth/devices/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> logoutAuthDevice(String id,) async {
final response = await logoutAuthDeviceWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'DELETE /auth/devices' operation and returns the [Response].
Future<Response> logoutAuthDevicesWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/auth/devices';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<void> logoutAuthDevices() async {
final response = await logoutAuthDevicesWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response]. /// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
+135
View File
@@ -0,0 +1,135 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SessionsApi {
SessionsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'DELETE /sessions' operation and returns the [Response].
Future<Response> deleteAllSessionsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/sessions';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<void> deleteAllSessions() async {
final response = await deleteAllSessionsWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'DELETE /sessions/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> deleteSessionWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/sessions/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> deleteSession(String id,) async {
final response = await deleteSessionWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'GET /sessions' operation and returns the [Response].
Future<Response> getSessionsWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/sessions';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<List<SessionResponseDto>?> getSessions() async {
final response = await getSessionsWithHttpInfo();
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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<SessionResponseDto>') as List)
.cast<SessionResponseDto>()
.toList(growable: false);
}
return null;
}
}
+2 -8
View File
@@ -202,10 +202,6 @@ class ApiClient {
return AlbumCountResponseDto.fromJson(value); return AlbumCountResponseDto.fromJson(value);
case 'AlbumResponseDto': case 'AlbumResponseDto':
return AlbumResponseDto.fromJson(value); return AlbumResponseDto.fromJson(value);
case 'AlbumUserResponseDto':
return AlbumUserResponseDto.fromJson(value);
case 'AlbumUserRole':
return AlbumUserRoleTypeTransformer().decode(value);
case 'AllJobStatusResponseDto': case 'AllJobStatusResponseDto':
return AllJobStatusResponseDto.fromJson(value); return AllJobStatusResponseDto.fromJson(value);
case 'AssetBulkDeleteDto': case 'AssetBulkDeleteDto':
@@ -252,8 +248,6 @@ class ApiClient {
return AudioCodecTypeTransformer().decode(value); return AudioCodecTypeTransformer().decode(value);
case 'AuditDeletesResponseDto': case 'AuditDeletesResponseDto':
return AuditDeletesResponseDto.fromJson(value); return AuditDeletesResponseDto.fromJson(value);
case 'AuthDeviceResponseDto':
return AuthDeviceResponseDto.fromJson(value);
case 'BulkIdResponseDto': case 'BulkIdResponseDto':
return BulkIdResponseDto.fromJson(value); return BulkIdResponseDto.fromJson(value);
case 'BulkIdsDto': case 'BulkIdsDto':
@@ -432,6 +426,8 @@ class ApiClient {
return ServerThemeDto.fromJson(value); return ServerThemeDto.fromJson(value);
case 'ServerVersionResponseDto': case 'ServerVersionResponseDto':
return ServerVersionResponseDto.fromJson(value); return ServerVersionResponseDto.fromJson(value);
case 'SessionResponseDto':
return SessionResponseDto.fromJson(value);
case 'SharedLinkCreateDto': case 'SharedLinkCreateDto':
return SharedLinkCreateDto.fromJson(value); return SharedLinkCreateDto.fromJson(value);
case 'SharedLinkEditDto': case 'SharedLinkEditDto':
@@ -504,8 +500,6 @@ class ApiClient {
return TranscodePolicyTypeTransformer().decode(value); return TranscodePolicyTypeTransformer().decode(value);
case 'UpdateAlbumDto': case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value); return UpdateAlbumDto.fromJson(value);
case 'UpdateAlbumUserDto':
return UpdateAlbumUserDto.fromJson(value);
case 'UpdateAssetDto': case 'UpdateAssetDto':
return UpdateAssetDto.fromJson(value); return UpdateAssetDto.fromJson(value);
case 'UpdateLibraryDto': case 'UpdateLibraryDto':
@@ -10,9 +10,9 @@
part of openapi.api; part of openapi.api;
class AuthDeviceResponseDto { class SessionResponseDto {
/// Returns a new [AuthDeviceResponseDto] instance. /// Returns a new [SessionResponseDto] instance.
AuthDeviceResponseDto({ SessionResponseDto({
required this.createdAt, required this.createdAt,
required this.current, required this.current,
required this.deviceOS, required this.deviceOS,
@@ -34,7 +34,7 @@ class AuthDeviceResponseDto {
String updatedAt; String updatedAt;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AuthDeviceResponseDto && bool operator ==(Object other) => identical(this, other) || other is SessionResponseDto &&
other.createdAt == createdAt && other.createdAt == createdAt &&
other.current == current && other.current == current &&
other.deviceOS == deviceOS && other.deviceOS == deviceOS &&
@@ -53,7 +53,7 @@ class AuthDeviceResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'AuthDeviceResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]'; String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -66,14 +66,14 @@ class AuthDeviceResponseDto {
return json; return json;
} }
/// Returns a new [AuthDeviceResponseDto] instance and imports its values from /// Returns a new [SessionResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise. /// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static AuthDeviceResponseDto? fromJson(dynamic value) { static SessionResponseDto? fromJson(dynamic value) {
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AuthDeviceResponseDto( return SessionResponseDto(
createdAt: mapValueOfType<String>(json, r'createdAt')!, createdAt: mapValueOfType<String>(json, r'createdAt')!,
current: mapValueOfType<bool>(json, r'current')!, current: mapValueOfType<bool>(json, r'current')!,
deviceOS: mapValueOfType<String>(json, r'deviceOS')!, deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
@@ -85,11 +85,11 @@ class AuthDeviceResponseDto {
return null; return null;
} }
static List<AuthDeviceResponseDto> listFromJson(dynamic json, {bool growable = false,}) { static List<SessionResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AuthDeviceResponseDto>[]; final result = <SessionResponseDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = AuthDeviceResponseDto.fromJson(row); final value = SessionResponseDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -98,12 +98,12 @@ class AuthDeviceResponseDto {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, AuthDeviceResponseDto> mapFromJson(dynamic json) { static Map<String, SessionResponseDto> mapFromJson(dynamic json) {
final map = <String, AuthDeviceResponseDto>{}; final map = <String, SessionResponseDto>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = AuthDeviceResponseDto.fromJson(entry.value); final value = SessionResponseDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -112,14 +112,14 @@ class AuthDeviceResponseDto {
return map; return map;
} }
// maps a json object with a list of AuthDeviceResponseDto-objects as value to a dart map // maps a json object with a list of SessionResponseDto-objects as value to a dart map
static Map<String, List<AuthDeviceResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<SessionResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AuthDeviceResponseDto>>{}; final map = <String, List<SessionResponseDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments // ignore: parameter_assignments
json = json.cast<String, dynamic>(); json = json.cast<String, dynamic>();
for (final entry in json.entries) { for (final entry in json.entries) {
map[entry.key] = AuthDeviceResponseDto.listFromJson(entry.value, growable: growable,); map[entry.key] = SessionResponseDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;
-15
View File
@@ -22,11 +22,6 @@ void main() {
// TODO // TODO
}); });
//Future<List<AuthDeviceResponseDto>> getAuthDevices() async
test('test getAuthDevices', () async {
// TODO
});
//Future<LoginResponseDto> login(LoginCredentialDto loginCredentialDto) async //Future<LoginResponseDto> login(LoginCredentialDto loginCredentialDto) async
test('test login', () async { test('test login', () async {
// TODO // TODO
@@ -37,16 +32,6 @@ void main() {
// TODO // TODO
}); });
//Future logoutAuthDevice(String id) async
test('test logoutAuthDevice', () async {
// TODO
});
//Future logoutAuthDevices() async
test('test logoutAuthDevices', () async {
// TODO
});
//Future<UserResponseDto> signUpAdmin(SignUpDto signUpDto) async //Future<UserResponseDto> signUpAdmin(SignUpDto signUpDto) async
test('test signUpAdmin', () async { test('test signUpAdmin', () async {
// TODO // TODO
@@ -11,11 +11,11 @@
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
// tests for AuthDeviceResponseDto // tests for SessionResponseDto
void main() { void main() {
// final instance = AuthDeviceResponseDto(); // final instance = SessionResponseDto();
group('test AuthDeviceResponseDto', () { group('test SessionResponseDto', () {
// String createdAt // String createdAt
test('to test the property `createdAt`', () async { test('to test the property `createdAt`', () async {
// TODO // TODO
+36
View File
@@ -0,0 +1,36 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
/// tests for SessionsApi
void main() {
// final instance = SessionsApi();
group('tests for SessionsApi', () {
//Future deleteAllSessions() async
test('test deleteAllSessions', () async {
// TODO
});
//Future deleteSession(String id) async
test('test deleteSession', () async {
// TODO
});
//Future<List<SessionResponseDto>> getSessions() async
test('test getSessions', () async {
// TODO
});
});
}
-3
View File
@@ -105,9 +105,6 @@ flutter:
- assets/ - assets/
- assets/i18n/ - assets/i18n/
fonts: fonts:
- family: SnowburstOne
fonts:
- asset: fonts/SnowburstOne.ttf
- family: Inconsolata - family: Inconsolata
fonts: fonts:
- asset: fonts/Inconsolata-Regular.ttf - asset: fonts/Inconsolata-Regular.ttf
+124 -124
View File
@@ -2583,99 +2583,6 @@
] ]
} }
}, },
"/auth/devices": {
"delete": {
"operationId": "logoutAuthDevices",
"parameters": [],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Authentication"
]
},
"get": {
"operationId": "getAuthDevices",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/AuthDeviceResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Authentication"
]
}
},
"/auth/devices/{id}": {
"delete": {
"operationId": "logoutAuthDevice",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Authentication"
]
}
},
"/auth/login": { "/auth/login": {
"post": { "post": {
"operationId": "login", "operationId": "login",
@@ -5237,6 +5144,99 @@
] ]
} }
}, },
"/sessions": {
"delete": {
"operationId": "deleteAllSessions",
"parameters": [],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Sessions"
]
},
"get": {
"operationId": "getSessions",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/SessionResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Sessions"
]
}
},
"/sessions/{id}": {
"delete": {
"operationId": "deleteSession",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Sessions"
]
}
},
"/shared-link": { "/shared-link": {
"get": { "get": {
"operationId": "getAllSharedLinks", "operationId": "getAllSharedLinks",
@@ -7976,37 +7976,6 @@
], ],
"type": "object" "type": "object"
}, },
"AuthDeviceResponseDto": {
"properties": {
"createdAt": {
"type": "string"
},
"current": {
"type": "boolean"
},
"deviceOS": {
"type": "string"
},
"deviceType": {
"type": "string"
},
"id": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
},
"required": [
"createdAt",
"current",
"deviceOS",
"deviceType",
"id",
"updatedAt"
],
"type": "object"
},
"BulkIdResponseDto": { "BulkIdResponseDto": {
"properties": { "properties": {
"error": { "error": {
@@ -10133,6 +10102,37 @@
], ],
"type": "object" "type": "object"
}, },
"SessionResponseDto": {
"properties": {
"createdAt": {
"type": "string"
},
"current": {
"type": "boolean"
},
"deviceOS": {
"type": "string"
},
"deviceType": {
"type": "string"
},
"id": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
},
"required": [
"createdAt",
"current",
"deviceOS",
"deviceType",
"id",
"updatedAt"
],
"type": "object"
},
"SharedLinkCreateDto": { "SharedLinkCreateDto": {
"properties": { "properties": {
"albumId": { "albumId": {
+30 -30
View File
@@ -355,14 +355,6 @@ export type ChangePasswordDto = {
newPassword: string; newPassword: string;
password: string; password: string;
}; };
export type AuthDeviceResponseDto = {
createdAt: string;
current: boolean;
deviceOS: string;
deviceType: string;
id: string;
updatedAt: string;
};
export type LoginCredentialDto = { export type LoginCredentialDto = {
email: string; email: string;
password: string; password: string;
@@ -800,6 +792,14 @@ export type ServerVersionResponseDto = {
minor: number; minor: number;
patch: number; patch: number;
}; };
export type SessionResponseDto = {
createdAt: string;
current: boolean;
deviceOS: string;
deviceType: string;
id: string;
updatedAt: string;
};
export type SharedLinkResponseDto = { export type SharedLinkResponseDto = {
album?: AlbumResponseDto; album?: AlbumResponseDto;
allowDownload: boolean; allowDownload: boolean;
@@ -1723,28 +1723,6 @@ export function changePassword({ changePasswordDto }: {
body: changePasswordDto body: changePasswordDto
}))); })));
} }
export function logoutAuthDevices(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/auth/devices", {
...opts,
method: "DELETE"
}));
}
export function getAuthDevices(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AuthDeviceResponseDto[];
}>("/auth/devices", {
...opts
}));
}
export function logoutAuthDevice({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/auth/devices/${encodeURIComponent(id)}`, {
...opts,
method: "DELETE"
}));
}
export function login({ loginCredentialDto }: { export function login({ loginCredentialDto }: {
loginCredentialDto: LoginCredentialDto; loginCredentialDto: LoginCredentialDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
@@ -2433,6 +2411,28 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) {
...opts ...opts
})); }));
} }
export function deleteAllSessions(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/sessions", {
...opts,
method: "DELETE"
}));
}
export function getSessions(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SessionResponseDto[];
}>("/sessions", {
...opts
}));
}
export function deleteSession({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/sessions/${encodeURIComponent(id)}`, {
...opts,
method: "DELETE"
}));
}
export function getAllSharedLinks(opts?: Oazapfts.RequestOpts) { export function getAllSharedLinks(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
+1 -1
View File
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
# web build # web build
FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e as web FROM node:iron-alpine3.18@sha256:d328c7bc3305e1ab26491817936c8151a47a8861ad617c16c1eeaa9c8075c8f6 as web
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+1 -11
View File
@@ -1,5 +1,4 @@
import { Command, CommandRunner } from 'nest-commander'; import { Command, CommandRunner } from 'nest-commander';
import { UserEntity } from 'src/entities/user.entity';
import { UserService } from 'src/services/user.service'; import { UserService } from 'src/services/user.service';
@Command({ @Command({
@@ -13,16 +12,7 @@ export class ListUsersCommand extends CommandRunner {
async run(): Promise<void> { async run(): Promise<void> {
try { try {
const users = await this.userService.getAll( const users = await this.userService.listUsers();
{
user: {
id: 'cli',
email: 'cli@immich.app',
isAdmin: true,
} as UserEntity,
},
true,
);
console.dir(users); console.dir(users);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
+1 -20
View File
@@ -1,9 +1,8 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants'; import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants';
import { import {
AuthDeviceResponseDto,
AuthDto, AuthDto,
ChangePasswordDto, ChangePasswordDto,
LoginCredentialDto, LoginCredentialDto,
@@ -15,7 +14,6 @@ import {
import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard'; import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
import { AuthService, LoginDetails } from 'src/services/auth.service'; import { AuthService, LoginDetails } from 'src/services/auth.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Authentication') @ApiTags('Authentication')
@Controller('auth') @Controller('auth')
@@ -41,23 +39,6 @@ export class AuthController {
return this.service.adminSignUp(dto); return this.service.adminSignUp(dto);
} }
@Get('devices')
getAuthDevices(@Auth() auth: AuthDto): Promise<AuthDeviceResponseDto[]> {
return this.service.getDevices(auth);
}
@Delete('devices')
@HttpCode(HttpStatus.NO_CONTENT)
logoutAuthDevices(@Auth() auth: AuthDto): Promise<void> {
return this.service.logoutDevices(auth);
}
@Delete('devices/:id')
@HttpCode(HttpStatus.NO_CONTENT)
logoutAuthDevice(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.logoutDevice(auth, id);
}
@Post('validateToken') @Post('validateToken')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
validateAccessToken(): ValidateAccessTokenResponseDto { validateAccessToken(): ValidateAccessTokenResponseDto {
+2
View File
@@ -16,6 +16,7 @@ import { PartnerController } from 'src/controllers/partner.controller';
import { PersonController } from 'src/controllers/person.controller'; import { PersonController } from 'src/controllers/person.controller';
import { SearchController } from 'src/controllers/search.controller'; import { SearchController } from 'src/controllers/search.controller';
import { ServerInfoController } from 'src/controllers/server-info.controller'; import { ServerInfoController } from 'src/controllers/server-info.controller';
import { SessionController } from 'src/controllers/session.controller';
import { SharedLinkController } from 'src/controllers/shared-link.controller'; import { SharedLinkController } from 'src/controllers/shared-link.controller';
import { SyncController } from 'src/controllers/sync.controller'; import { SyncController } from 'src/controllers/sync.controller';
import { SystemConfigController } from 'src/controllers/system-config.controller'; import { SystemConfigController } from 'src/controllers/system-config.controller';
@@ -43,6 +44,7 @@ export const controllers = [
PartnerController, PartnerController,
SearchController, SearchController,
ServerInfoController, ServerInfoController,
SessionController,
SharedLinkController, SharedLinkController,
SyncController, SyncController,
SystemConfigController, SystemConfigController,
@@ -0,0 +1,31 @@
import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto } from 'src/dtos/session.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SessionService } from 'src/services/session.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Sessions')
@Controller('sessions')
@Authenticated()
export class SessionController {
constructor(private service: SessionService) {}
@Get()
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
return this.service.getAll(auth);
}
@Delete()
@HttpCode(HttpStatus.NO_CONTENT)
deleteAllSessions(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteAll(auth);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
}
+2 -20
View File
@@ -2,8 +2,8 @@ import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
import { APIKeyEntity } from 'src/entities/api-key.entity'; import { APIKeyEntity } from 'src/entities/api-key.entity';
import { SessionEntity } from 'src/entities/session.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
export class AuthDto { export class AuthDto {
@@ -11,7 +11,7 @@ export class AuthDto {
apiKey?: APIKeyEntity; apiKey?: APIKeyEntity;
sharedLink?: SharedLinkEntity; sharedLink?: SharedLinkEntity;
userToken?: UserTokenEntity; session?: SessionEntity;
} }
export class LoginCredentialDto { export class LoginCredentialDto {
@@ -78,24 +78,6 @@ export class ValidateAccessTokenResponseDto {
authStatus!: boolean; authStatus!: boolean;
} }
export class AuthDeviceResponseDto {
id!: string;
createdAt!: string;
updatedAt!: string;
current!: boolean;
deviceType!: string;
deviceOS!: string;
}
export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({
id: entity.id,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
current: currentId === entity.id,
deviceOS: entity.deviceOS,
deviceType: entity.deviceType,
});
export class OAuthCallbackDto { export class OAuthCallbackDto {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
+19
View File
@@ -0,0 +1,19 @@
import { SessionEntity } from 'src/entities/session.entity';
export class SessionResponseDto {
id!: string;
createdAt!: string;
updatedAt!: string;
current!: boolean;
deviceType!: string;
deviceOS!: string;
}
export const mapSession = (entity: SessionEntity, currentId?: string): SessionResponseDto => ({
id: entity.id,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
current: currentId === entity.id,
deviceOS: entity.deviceOS,
deviceType: entity.deviceType,
});
+2 -2
View File
@@ -14,13 +14,13 @@ import { MemoryEntity } from 'src/entities/memory.entity';
import { MoveEntity } from 'src/entities/move.entity'; import { MoveEntity } from 'src/entities/move.entity';
import { PartnerEntity } from 'src/entities/partner.entity'; import { PartnerEntity } from 'src/entities/partner.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { SessionEntity } from 'src/entities/session.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { SmartSearchEntity } from 'src/entities/smart-search.entity'; import { SmartSearchEntity } from 'src/entities/smart-search.entity';
import { SystemConfigEntity } from 'src/entities/system-config.entity'; import { SystemConfigEntity } from 'src/entities/system-config.entity';
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity'; import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
import { TagEntity } from 'src/entities/tag.entity'; import { TagEntity } from 'src/entities/tag.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
export const entities = [ export const entities = [
@@ -46,6 +46,6 @@ export const entities = [
SystemMetadataEntity, SystemMetadataEntity,
TagEntity, TagEntity,
UserEntity, UserEntity,
UserTokenEntity, SessionEntity,
LibraryEntity, LibraryEntity,
]; ];
@@ -1,8 +1,8 @@
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
@Entity('user_token') @Entity('sessions')
export class UserTokenEntity { export class SessionEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id!: string; id!: string;
@@ -0,0 +1,11 @@
import { SessionEntity } from 'src/entities/session.entity';
export const ISessionRepository = 'ISessionRepository';
export interface ISessionRepository {
create(dto: Partial<SessionEntity>): Promise<SessionEntity>;
update(dto: Partial<SessionEntity>): Promise<SessionEntity>;
delete(id: string): Promise<void>;
getByToken(token: string): Promise<SessionEntity | null>;
getByUserId(userId: string): Promise<SessionEntity[]>;
}
@@ -1,11 +0,0 @@
import { UserTokenEntity } from 'src/entities/user-token.entity';
export const IUserTokenRepository = 'IUserTokenRepository';
export interface IUserTokenRepository {
create(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
save(dto: Partial<UserTokenEntity>): Promise<UserTokenEntity>;
delete(id: string): Promise<void>;
getByToken(token: string): Promise<UserTokenEntity | null>;
getAll(userId: string): Promise<UserTokenEntity[]>;
}
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RenameSessionsTable1713490844785 implements MigrationInterface {
name = 'RenameSessionsTable1713490844785';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_token" RENAME TO "sessions"`);
await queryRunner.query(`ALTER TABLE "sessions" RENAME CONSTRAINT "FK_d37db50eecdf9b8ce4eedd2f918" to "FK_57de40bc620f456c7311aa3a1e6"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "sessions" RENAME CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" to "FK_d37db50eecdf9b8ce4eedd2f918"`);
await queryRunner.query(`ALTER TABLE "sessions" RENAME TO "user_token"`);
}
}
+4 -4
View File
@@ -185,13 +185,13 @@ WHERE
-- AccessRepository.authDevice.checkOwnerAccess -- AccessRepository.authDevice.checkOwnerAccess
SELECT SELECT
"UserTokenEntity"."id" AS "UserTokenEntity_id" "SessionEntity"."id" AS "SessionEntity_id"
FROM FROM
"user_token" "UserTokenEntity" "sessions" "SessionEntity"
WHERE WHERE
( (
("UserTokenEntity"."userId" = $1) ("SessionEntity"."userId" = $1)
AND ("UserTokenEntity"."id" IN ($2)) AND ("SessionEntity"."id" IN ($2))
) )
-- AccessRepository.library.checkOwnerAccess -- AccessRepository.library.checkOwnerAccess
+48
View File
@@ -0,0 +1,48 @@
-- NOTE: This file is auto generated by ./sql-generator
-- SessionRepository.getByToken
SELECT DISTINCT
"distinctAlias"."SessionEntity_id" AS "ids_SessionEntity_id"
FROM
(
SELECT
"SessionEntity"."id" AS "SessionEntity_id",
"SessionEntity"."userId" AS "SessionEntity_userId",
"SessionEntity"."createdAt" AS "SessionEntity_createdAt",
"SessionEntity"."updatedAt" AS "SessionEntity_updatedAt",
"SessionEntity"."deviceType" AS "SessionEntity_deviceType",
"SessionEntity"."deviceOS" AS "SessionEntity_deviceOS",
"SessionEntity__SessionEntity_user"."id" AS "SessionEntity__SessionEntity_user_id",
"SessionEntity__SessionEntity_user"."name" AS "SessionEntity__SessionEntity_user_name",
"SessionEntity__SessionEntity_user"."avatarColor" AS "SessionEntity__SessionEntity_user_avatarColor",
"SessionEntity__SessionEntity_user"."isAdmin" AS "SessionEntity__SessionEntity_user_isAdmin",
"SessionEntity__SessionEntity_user"."email" AS "SessionEntity__SessionEntity_user_email",
"SessionEntity__SessionEntity_user"."storageLabel" AS "SessionEntity__SessionEntity_user_storageLabel",
"SessionEntity__SessionEntity_user"."oauthId" AS "SessionEntity__SessionEntity_user_oauthId",
"SessionEntity__SessionEntity_user"."profileImagePath" AS "SessionEntity__SessionEntity_user_profileImagePath",
"SessionEntity__SessionEntity_user"."shouldChangePassword" AS "SessionEntity__SessionEntity_user_shouldChangePassword",
"SessionEntity__SessionEntity_user"."createdAt" AS "SessionEntity__SessionEntity_user_createdAt",
"SessionEntity__SessionEntity_user"."deletedAt" AS "SessionEntity__SessionEntity_user_deletedAt",
"SessionEntity__SessionEntity_user"."status" AS "SessionEntity__SessionEntity_user_status",
"SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt",
"SessionEntity__SessionEntity_user"."memoriesEnabled" AS "SessionEntity__SessionEntity_user_memoriesEnabled",
"SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes",
"SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes"
FROM
"sessions" "SessionEntity"
LEFT JOIN "users" "SessionEntity__SessionEntity_user" ON "SessionEntity__SessionEntity_user"."id" = "SessionEntity"."userId"
AND (
"SessionEntity__SessionEntity_user"."deletedAt" IS NULL
)
WHERE
(("SessionEntity"."token" = $1))
) "distinctAlias"
ORDER BY
"SessionEntity_id" ASC
LIMIT
1
-- SessionRepository.delete
DELETE FROM "sessions"
WHERE
"id" = $1
@@ -1,48 +0,0 @@
-- NOTE: This file is auto generated by ./sql-generator
-- UserTokenRepository.getByToken
SELECT DISTINCT
"distinctAlias"."UserTokenEntity_id" AS "ids_UserTokenEntity_id"
FROM
(
SELECT
"UserTokenEntity"."id" AS "UserTokenEntity_id",
"UserTokenEntity"."userId" AS "UserTokenEntity_userId",
"UserTokenEntity"."createdAt" AS "UserTokenEntity_createdAt",
"UserTokenEntity"."updatedAt" AS "UserTokenEntity_updatedAt",
"UserTokenEntity"."deviceType" AS "UserTokenEntity_deviceType",
"UserTokenEntity"."deviceOS" AS "UserTokenEntity_deviceOS",
"UserTokenEntity__UserTokenEntity_user"."id" AS "UserTokenEntity__UserTokenEntity_user_id",
"UserTokenEntity__UserTokenEntity_user"."name" AS "UserTokenEntity__UserTokenEntity_user_name",
"UserTokenEntity__UserTokenEntity_user"."avatarColor" AS "UserTokenEntity__UserTokenEntity_user_avatarColor",
"UserTokenEntity__UserTokenEntity_user"."isAdmin" AS "UserTokenEntity__UserTokenEntity_user_isAdmin",
"UserTokenEntity__UserTokenEntity_user"."email" AS "UserTokenEntity__UserTokenEntity_user_email",
"UserTokenEntity__UserTokenEntity_user"."storageLabel" AS "UserTokenEntity__UserTokenEntity_user_storageLabel",
"UserTokenEntity__UserTokenEntity_user"."oauthId" AS "UserTokenEntity__UserTokenEntity_user_oauthId",
"UserTokenEntity__UserTokenEntity_user"."profileImagePath" AS "UserTokenEntity__UserTokenEntity_user_profileImagePath",
"UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword",
"UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt",
"UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt",
"UserTokenEntity__UserTokenEntity_user"."status" AS "UserTokenEntity__UserTokenEntity_user_status",
"UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt",
"UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled",
"UserTokenEntity__UserTokenEntity_user"."quotaSizeInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaSizeInBytes",
"UserTokenEntity__UserTokenEntity_user"."quotaUsageInBytes" AS "UserTokenEntity__UserTokenEntity_user_quotaUsageInBytes"
FROM
"user_token" "UserTokenEntity"
LEFT JOIN "users" "UserTokenEntity__UserTokenEntity_user" ON "UserTokenEntity__UserTokenEntity_user"."id" = "UserTokenEntity"."userId"
AND (
"UserTokenEntity__UserTokenEntity_user"."deletedAt" IS NULL
)
WHERE
(("UserTokenEntity"."token" = $1))
) "distinctAlias"
ORDER BY
"UserTokenEntity_id" ASC
LIMIT
1
-- UserTokenRepository.delete
DELETE FROM "user_token"
WHERE
"id" = $1
+5 -5
View File
@@ -10,8 +10,8 @@ import { LibraryEntity } from 'src/entities/library.entity';
import { MemoryEntity } from 'src/entities/memory.entity'; import { MemoryEntity } from 'src/entities/memory.entity';
import { PartnerEntity } from 'src/entities/partner.entity'; import { PartnerEntity } from 'src/entities/partner.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { SessionEntity } from 'src/entities/session.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { Instrumentation } from 'src/utils/instrumentation'; import { Instrumentation } from 'src/utils/instrumentation';
import { Brackets, In, Repository } from 'typeorm'; import { Brackets, In, Repository } from 'typeorm';
@@ -293,7 +293,7 @@ class AssetAccess implements IAssetAccess {
} }
class AuthDeviceAccess implements IAuthDeviceAccess { class AuthDeviceAccess implements IAuthDeviceAccess {
constructor(private tokenRepository: Repository<UserTokenEntity>) {} constructor(private sessionRepository: Repository<SessionEntity>) {}
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 }) @ChunkedSet({ paramIndex: 1 })
@@ -302,7 +302,7 @@ class AuthDeviceAccess implements IAuthDeviceAccess {
return new Set(); return new Set();
} }
return this.tokenRepository return this.sessionRepository
.find({ .find({
select: { id: true }, select: { id: true },
where: { where: {
@@ -464,12 +464,12 @@ export class AccessRepository implements IAccessRepository {
@InjectRepository(PersonEntity) personRepository: Repository<PersonEntity>, @InjectRepository(PersonEntity) personRepository: Repository<PersonEntity>,
@InjectRepository(AssetFaceEntity) assetFaceRepository: Repository<AssetFaceEntity>, @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository<AssetFaceEntity>,
@InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository<SharedLinkEntity>, @InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository<SharedLinkEntity>,
@InjectRepository(UserTokenEntity) tokenRepository: Repository<UserTokenEntity>, @InjectRepository(SessionEntity) sessionRepository: Repository<SessionEntity>,
) { ) {
this.activity = new ActivityAccess(activityRepository, albumRepository); this.activity = new ActivityAccess(activityRepository, albumRepository);
this.album = new AlbumAccess(albumRepository, sharedLinkRepository); this.album = new AlbumAccess(albumRepository, sharedLinkRepository);
this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository); this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository);
this.authDevice = new AuthDeviceAccess(tokenRepository); this.authDevice = new AuthDeviceAccess(sessionRepository);
this.library = new LibraryAccess(libraryRepository); this.library = new LibraryAccess(libraryRepository);
this.memory = new MemoryAccess(memoryRepository); this.memory = new MemoryAccess(memoryRepository);
this.person = new PersonAccess(assetFaceRepository, personRepository); this.person = new PersonAccess(assetFaceRepository, personRepository);
+3 -3
View File
@@ -23,12 +23,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface'; import { IPersonRepository } from 'src/interfaces/person.interface';
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface'; import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { AccessRepository } from 'src/repositories/access.repository'; import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository'; import { ActivityRepository } from 'src/repositories/activity.repository';
@@ -55,12 +55,12 @@ import { PartnerRepository } from 'src/repositories/partner.repository';
import { PersonRepository } from 'src/repositories/person.repository'; import { PersonRepository } from 'src/repositories/person.repository';
import { SearchRepository } from 'src/repositories/search.repository'; import { SearchRepository } from 'src/repositories/search.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository'; import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
import { StorageRepository } from 'src/repositories/storage.repository'; import { StorageRepository } from 'src/repositories/storage.repository';
import { SystemConfigRepository } from 'src/repositories/system-config.repository'; import { SystemConfigRepository } from 'src/repositories/system-config.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TagRepository } from 'src/repositories/tag.repository'; import { TagRepository } from 'src/repositories/tag.repository';
import { UserTokenRepository } from 'src/repositories/user-token.repository';
import { UserRepository } from 'src/repositories/user.repository'; import { UserRepository } from 'src/repositories/user.repository';
export const repositories = [ export const repositories = [
@@ -89,11 +89,11 @@ export const repositories = [
{ provide: IServerInfoRepository, useClass: ServerInfoRepository }, { provide: IServerInfoRepository, useClass: ServerInfoRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: ISearchRepository, useClass: SearchRepository }, { provide: ISearchRepository, useClass: SearchRepository },
{ provide: ISessionRepository, useClass: SessionRepository },
{ provide: IStorageRepository, useClass: StorageRepository }, { provide: IStorageRepository, useClass: StorageRepository },
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository },
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
{ provide: ITagRepository, useClass: TagRepository }, { provide: ITagRepository, useClass: TagRepository },
{ provide: IMediaRepository, useClass: MediaRepository }, { provide: IMediaRepository, useClass: MediaRepository },
{ provide: IUserRepository, useClass: UserRepository }, { provide: IUserRepository, useClass: UserRepository },
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
]; ];
@@ -1,22 +1,22 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators'; import { DummyValue, GenerateSql } from 'src/decorators';
import { UserTokenEntity } from 'src/entities/user-token.entity'; import { SessionEntity } from 'src/entities/session.entity';
import { IUserTokenRepository } from 'src/interfaces/user-token.interface'; import { ISessionRepository } from 'src/interfaces/session.interface';
import { Instrumentation } from 'src/utils/instrumentation'; import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@Instrumentation() @Instrumentation()
@Injectable() @Injectable()
export class UserTokenRepository implements IUserTokenRepository { export class SessionRepository implements ISessionRepository {
constructor(@InjectRepository(UserTokenEntity) private repository: Repository<UserTokenEntity>) {} constructor(@InjectRepository(SessionEntity) private repository: Repository<SessionEntity>) {}
@GenerateSql({ params: [DummyValue.STRING] }) @GenerateSql({ params: [DummyValue.STRING] })
getByToken(token: string): Promise<UserTokenEntity | null> { getByToken(token: string): Promise<SessionEntity | null> {
return this.repository.findOne({ where: { token }, relations: { user: true } }); return this.repository.findOne({ where: { token }, relations: { user: true } });
} }
getAll(userId: string): Promise<UserTokenEntity[]> { getByUserId(userId: string): Promise<SessionEntity[]> {
return this.repository.find({ return this.repository.find({
where: { where: {
userId, userId,
@@ -31,12 +31,12 @@ export class UserTokenRepository implements IUserTokenRepository {
}); });
} }
create(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> { create(session: Partial<SessionEntity>): Promise<SessionEntity> {
return this.repository.save(userToken); return this.repository.save(session);
} }
save(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> { update(session: Partial<SessionEntity>): Promise<SessionEntity> {
return this.repository.save(userToken); return this.repository.save(session);
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
+23 -80
View File
@@ -9,25 +9,25 @@ import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { AuthService } from 'src/services/auth.service'; import { AuthService } from 'src/services/auth.service';
import { keyStub } from 'test/fixtures/api-key.stub'; import { keyStub } from 'test/fixtures/api-key.stub';
import { authStub, loginResponseStub } from 'test/fixtures/auth.stub'; import { authStub, loginResponseStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub';
import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userTokenStub } from 'test/fixtures/user-token.stub';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock'; import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock'; import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock'; import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
import { newUserTokenRepositoryMock } from 'test/repositories/user-token.repository.mock';
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
import { Mock, Mocked, vitest } from 'vitest'; import { Mock, Mocked, vitest } from 'vitest';
@@ -65,7 +65,7 @@ describe('AuthService', () => {
let libraryMock: Mocked<ILibraryRepository>; let libraryMock: Mocked<ILibraryRepository>;
let loggerMock: Mocked<ILoggerRepository>; let loggerMock: Mocked<ILoggerRepository>;
let configMock: Mocked<ISystemConfigRepository>; let configMock: Mocked<ISystemConfigRepository>;
let userTokenMock: Mocked<IUserTokenRepository>; let sessionMock: Mocked<ISessionRepository>;
let shareMock: Mocked<ISharedLinkRepository>; let shareMock: Mocked<ISharedLinkRepository>;
let keyMock: Mocked<IKeyRepository>; let keyMock: Mocked<IKeyRepository>;
@@ -98,7 +98,7 @@ describe('AuthService', () => {
libraryMock = newLibraryRepositoryMock(); libraryMock = newLibraryRepositoryMock();
loggerMock = newLoggerRepositoryMock(); loggerMock = newLoggerRepositoryMock();
configMock = newSystemConfigRepositoryMock(); configMock = newSystemConfigRepositoryMock();
userTokenMock = newUserTokenRepositoryMock(); sessionMock = newSessionRepositoryMock();
shareMock = newSharedLinkRepositoryMock(); shareMock = newSharedLinkRepositoryMock();
keyMock = newKeyRepositoryMock(); keyMock = newKeyRepositoryMock();
@@ -109,7 +109,7 @@ describe('AuthService', () => {
libraryMock, libraryMock,
loggerMock, loggerMock,
userMock, userMock,
userTokenMock, sessionMock,
shareMock, shareMock,
keyMock, keyMock,
); );
@@ -139,14 +139,14 @@ describe('AuthService', () => {
it('should successfully log the user in', async () => { it('should successfully log the user in', async () => {
userMock.getByEmail.mockResolvedValue(userStub.user1); userMock.getByEmail.mockResolvedValue(userStub.user1);
userTokenMock.create.mockResolvedValue(userTokenStub.userToken); sessionMock.create.mockResolvedValue(sessionStub.valid);
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password); await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
expect(userMock.getByEmail).toHaveBeenCalledTimes(1); expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
}); });
it('should generate the cookie headers (insecure)', async () => { it('should generate the cookie headers (insecure)', async () => {
userMock.getByEmail.mockResolvedValue(userStub.user1); userMock.getByEmail.mockResolvedValue(userStub.user1);
userTokenMock.create.mockResolvedValue(userTokenStub.userToken); sessionMock.create.mockResolvedValue(sessionStub.valid);
await expect( await expect(
sut.login(fixtures.login, { sut.login(fixtures.login, {
clientIp: '127.0.0.1', clientIp: '127.0.0.1',
@@ -231,14 +231,14 @@ describe('AuthService', () => {
}); });
it('should delete the access token', async () => { it('should delete the access token', async () => {
const auth = { user: { id: '123' }, userToken: { id: 'token123' } } as AuthDto; const auth = { user: { id: '123' }, session: { id: 'token123' } } as AuthDto;
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
successful: true, successful: true,
redirectUri: '/auth/login?autoLaunch=0', redirectUri: '/auth/login?autoLaunch=0',
}); });
expect(userTokenMock.delete).toHaveBeenCalledWith('token123'); expect(sessionMock.delete).toHaveBeenCalledWith('token123');
}); });
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => { it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
@@ -282,11 +282,11 @@ describe('AuthService', () => {
it('should validate using authorization header', async () => { it('should validate using authorization header', async () => {
userMock.get.mockResolvedValue(userStub.user1); userMock.get.mockResolvedValue(userStub.user1);
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken); sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
const client = { request: { headers: { authorization: 'Bearer auth_token' } } }; const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({ await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({
user: userStub.user1, user: userStub.user1,
userToken: userTokenStub.userToken, session: sessionStub.valid,
}); });
}); });
}); });
@@ -336,37 +336,29 @@ describe('AuthService', () => {
describe('validate - user token', () => { describe('validate - user token', () => {
it('should throw if no token is found', async () => { it('should throw if no token is found', async () => {
userTokenMock.getByToken.mockResolvedValue(null); sessionMock.getByToken.mockResolvedValue(null);
const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' }; const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' };
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
}); });
it('should return an auth dto', async () => { it('should return an auth dto', async () => {
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken); sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual({ await expect(sut.validate(headers, {})).resolves.toEqual({
user: userStub.user1, user: userStub.user1,
userToken: userTokenStub.userToken, session: sessionStub.valid,
}); });
}); });
it('should update when access time exceeds an hour', async () => { it('should update when access time exceeds an hour', async () => {
userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken); sessionMock.getByToken.mockResolvedValue(sessionStub.inactive);
userTokenMock.save.mockResolvedValue(userTokenStub.userToken); sessionMock.update.mockResolvedValue(sessionStub.valid);
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
await expect(sut.validate(headers, {})).resolves.toEqual({ await expect(sut.validate(headers, {})).resolves.toEqual({
user: userStub.user1, user: userStub.user1,
userToken: userTokenStub.userToken, session: sessionStub.valid,
});
expect(userTokenMock.save.mock.calls[0][0]).toMatchObject({
id: 'not_active',
token: 'auth_token',
userId: 'user-id',
createdAt: new Date('2021-01-01'),
updatedAt: expect.any(Date),
deviceOS: 'Android',
deviceType: 'Mobile',
}); });
expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) });
}); });
}); });
@@ -386,55 +378,6 @@ describe('AuthService', () => {
}); });
}); });
describe('getDevices', () => {
it('should get the devices', async () => {
userTokenMock.getAll.mockResolvedValue([userTokenStub.userToken, userTokenStub.inactiveToken]);
await expect(sut.getDevices(authStub.user1)).resolves.toEqual([
{
createdAt: '2021-01-01T00:00:00.000Z',
current: true,
deviceOS: '',
deviceType: '',
id: 'token-id',
updatedAt: expect.any(String),
},
{
createdAt: '2021-01-01T00:00:00.000Z',
current: false,
deviceOS: 'Android',
deviceType: 'Mobile',
id: 'not_active',
updatedAt: expect.any(String),
},
]);
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
});
});
describe('logoutDevices', () => {
it('should logout all devices', async () => {
userTokenMock.getAll.mockResolvedValue([userTokenStub.inactiveToken, userTokenStub.userToken]);
await sut.logoutDevices(authStub.user1);
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
expect(userTokenMock.delete).toHaveBeenCalledWith('not_active');
expect(userTokenMock.delete).not.toHaveBeenCalledWith('token-id');
});
});
describe('logoutDevice', () => {
it('should logout the device', async () => {
accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1']));
await sut.logoutDevice(authStub.user1, 'token-1');
expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1']));
expect(userTokenMock.delete).toHaveBeenCalledWith('token-1');
});
});
describe('getMobileRedirect', () => { describe('getMobileRedirect', () => {
it('should pass along the query params', () => { it('should pass along the query params', () => {
expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual('app.immich:/?code=123&state=456'); expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual('app.immich:/?code=123&state=456');
@@ -463,7 +406,7 @@ describe('AuthService', () => {
configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister); configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
userMock.getByEmail.mockResolvedValue(userStub.user1); userMock.getByEmail.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1); userMock.update.mockResolvedValue(userStub.user1);
userTokenMock.create.mockResolvedValue(userTokenStub.userToken); sessionMock.create.mockResolvedValue(sessionStub.valid);
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
loginResponseStub.user1oauth, loginResponseStub.user1oauth,
@@ -478,7 +421,7 @@ describe('AuthService', () => {
userMock.getByEmail.mockResolvedValue(null); userMock.getByEmail.mockResolvedValue(null);
userMock.getAdmin.mockResolvedValue(userStub.user1); userMock.getAdmin.mockResolvedValue(userStub.user1);
userMock.create.mockResolvedValue(userStub.user1); userMock.create.mockResolvedValue(userStub.user1);
userTokenMock.create.mockResolvedValue(userTokenStub.userToken); sessionMock.create.mockResolvedValue(sessionStub.valid);
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual( await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
loginResponseStub.user1oauth, loginResponseStub.user1oauth,
@@ -491,7 +434,7 @@ describe('AuthService', () => {
it('should use the mobile redirect override', async () => { it('should use the mobile redirect override', async () => {
configMock.load.mockResolvedValue(systemConfigStub.override); configMock.load.mockResolvedValue(systemConfigStub.override);
userMock.getByOAuthId.mockResolvedValue(userStub.user1); userMock.getByOAuthId.mockResolvedValue(userStub.user1);
userTokenMock.create.mockResolvedValue(userTokenStub.userToken); sessionMock.create.mockResolvedValue(sessionStub.valid);
await sut.callback({ url: `app.immich:/?code=abc123` }, loginDetails); await sut.callback({ url: `app.immich:/?code=abc123` }, loginDetails);
@@ -501,7 +444,7 @@ describe('AuthService', () => {
it('should use the mobile redirect override for ios urls with multiple slashes', async () => { it('should use the mobile redirect override for ios urls with multiple slashes', async () => {
configMock.load.mockResolvedValue(systemConfigStub.override); configMock.load.mockResolvedValue(systemConfigStub.override);
userMock.getByOAuthId.mockResolvedValue(userStub.user1); userMock.getByOAuthId.mockResolvedValue(userStub.user1);
userTokenMock.create.mockResolvedValue(userTokenStub.userToken); sessionMock.create.mockResolvedValue(sessionStub.valid);
await sut.callback({ url: `app.immich:///?code=abc123` }, loginDetails); await sut.callback({ url: `app.immich:///?code=abc123` }, loginDetails);
+17 -38
View File
@@ -19,11 +19,10 @@ import {
LOGIN_URL, LOGIN_URL,
MOBILE_REDIRECT, MOBILE_REDIRECT,
} from 'src/constants'; } from 'src/constants';
import { AccessCore, Permission } from 'src/cores/access.core'; import { AccessCore } from 'src/cores/access.core';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.core'; import { UserCore } from 'src/cores/user.core';
import { import {
AuthDeviceResponseDto,
AuthDto, AuthDto,
ChangePasswordDto, ChangePasswordDto,
LoginCredentialDto, LoginCredentialDto,
@@ -34,7 +33,6 @@ import {
OAuthConfigDto, OAuthConfigDto,
SignUpDto, SignUpDto,
mapLoginResponse, mapLoginResponse,
mapUserToken,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { SystemConfig } from 'src/entities/system-config.entity'; import { SystemConfig } from 'src/entities/system-config.entity';
@@ -44,9 +42,9 @@ import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface'; import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
import { HumanReadableSize } from 'src/utils/bytes'; import { HumanReadableSize } from 'src/utils/bytes';
@@ -85,7 +83,7 @@ export class AuthService {
@Inject(ILibraryRepository) libraryRepository: ILibraryRepository, @Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(IUserTokenRepository) private userTokenRepository: IUserTokenRepository, @Inject(ISessionRepository) private sessionRepository: ISessionRepository,
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository, @Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
@Inject(IKeyRepository) private keyRepository: IKeyRepository, @Inject(IKeyRepository) private keyRepository: IKeyRepository,
) { ) {
@@ -120,8 +118,8 @@ export class AuthService {
} }
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> { async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
if (auth.userToken) { if (auth.session) {
await this.userTokenRepository.delete(auth.userToken.id); await this.sessionRepository.delete(auth.session.id);
} }
return { return {
@@ -164,8 +162,9 @@ export class AuthService {
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> { async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
const shareKey = (headers['x-immich-share-key'] || params.key) as string; const shareKey = (headers['x-immich-share-key'] || params.key) as string;
const userToken = (headers['x-immich-user-token'] || const session = (headers['x-immich-user-token'] ||
params.userToken || headers['x-immich-session-token'] ||
params.sessionKey ||
this.getBearerToken(headers) || this.getBearerToken(headers) ||
this.getCookieToken(headers)) as string; this.getCookieToken(headers)) as string;
const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string; const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string;
@@ -174,8 +173,8 @@ export class AuthService {
return this.validateSharedLink(shareKey); return this.validateSharedLink(shareKey);
} }
if (userToken) { if (session) {
return this.validateUserToken(userToken); return this.validateSession(session);
} }
if (apiKey) { if (apiKey) {
@@ -185,26 +184,6 @@ export class AuthService {
throw new UnauthorizedException('Authentication required'); throw new UnauthorizedException('Authentication required');
} }
async getDevices(auth: AuthDto): Promise<AuthDeviceResponseDto[]> {
const userTokens = await this.userTokenRepository.getAll(auth.user.id);
return userTokens.map((userToken) => mapUserToken(userToken, auth.userToken?.id));
}
async logoutDevice(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id);
await this.userTokenRepository.delete(id);
}
async logoutDevices(auth: AuthDto): Promise<void> {
const devices = await this.userTokenRepository.getAll(auth.user.id);
for (const device of devices) {
if (device.id === auth.userToken?.id) {
continue;
}
await this.userTokenRepository.delete(device.id);
}
}
getMobileRedirect(url: string) { getMobileRedirect(url: string) {
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`; return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
} }
@@ -408,19 +387,19 @@ export class AuthService {
return this.cryptoRepository.compareBcrypt(inputPassword, user.password); return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
} }
private async validateUserToken(tokenValue: string): Promise<AuthDto> { private async validateSession(tokenValue: string): Promise<AuthDto> {
const hashedToken = this.cryptoRepository.hashSha256(tokenValue); const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
let userToken = await this.userTokenRepository.getByToken(hashedToken); let session = await this.sessionRepository.getByToken(hashedToken);
if (userToken?.user) { if (session?.user) {
const now = DateTime.now(); const now = DateTime.now();
const updatedAt = DateTime.fromJSDate(userToken.updatedAt); const updatedAt = DateTime.fromJSDate(session.updatedAt);
const diff = now.diff(updatedAt, ['hours']); const diff = now.diff(updatedAt, ['hours']);
if (diff.hours > 1) { if (diff.hours > 1) {
userToken = await this.userTokenRepository.save({ ...userToken, updatedAt: new Date() }); session = await this.sessionRepository.update({ id: session.id, updatedAt: new Date() });
} }
return { user: userToken.user, userToken }; return { user: session.user, session: session };
} }
throw new UnauthorizedException('Invalid user token'); throw new UnauthorizedException('Invalid user token');
@@ -430,7 +409,7 @@ export class AuthService {
const key = this.cryptoRepository.newPassword(32); const key = this.cryptoRepository.newPassword(32);
const token = this.cryptoRepository.hashSha256(key); const token = this.cryptoRepository.hashSha256(key);
await this.userTokenRepository.create({ await this.sessionRepository.create({
token, token,
user, user,
deviceOS: loginDetails.deviceOS, deviceOS: loginDetails.deviceOS,
+2
View File
@@ -18,6 +18,7 @@ import { PartnerService } from 'src/services/partner.service';
import { PersonService } from 'src/services/person.service'; import { PersonService } from 'src/services/person.service';
import { SearchService } from 'src/services/search.service'; import { SearchService } from 'src/services/search.service';
import { ServerInfoService } from 'src/services/server-info.service'; import { ServerInfoService } from 'src/services/server-info.service';
import { SessionService } from 'src/services/session.service';
import { SharedLinkService } from 'src/services/shared-link.service'; import { SharedLinkService } from 'src/services/shared-link.service';
import { SmartInfoService } from 'src/services/smart-info.service'; import { SmartInfoService } from 'src/services/smart-info.service';
import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageTemplateService } from 'src/services/storage-template.service';
@@ -50,6 +51,7 @@ export const services = [
PersonService, PersonService,
SearchService, SearchService,
ServerInfoService, ServerInfoService,
SessionService,
SharedLinkService, SharedLinkService,
SmartInfoService, SmartInfoService,
StorageService, StorageService,
+27
View File
@@ -225,6 +225,15 @@ describe(MediaService.name, () => {
expect(assetMock.update).not.toHaveBeenCalledWith(); expect(assetMock.update).not.toHaveBeenCalledWith();
}); });
it('should skip invisible assets', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
expect(await sut.handleGeneratePreview({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
expect(mediaMock.resize).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith();
});
it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => { it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]); configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]);
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
@@ -353,6 +362,15 @@ describe(MediaService.name, () => {
expect(assetMock.update).not.toHaveBeenCalledWith(); expect(assetMock.update).not.toHaveBeenCalledWith();
}); });
it('should skip invisible assets', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
expect(await sut.handleGenerateThumbnail({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
expect(mediaMock.resize).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith();
});
it.each(Object.values(ImageFormat))( it.each(Object.values(ImageFormat))(
'should generate a %s thumbnail for an image when specified', 'should generate a %s thumbnail for an image when specified',
async (format) => { async (format) => {
@@ -410,6 +428,15 @@ describe(MediaService.name, () => {
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
}); });
it('should skip invisible assets', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
expect(await sut.handleGenerateThumbhash({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith();
});
it('should generate a thumbhash', async () => { it('should generate a thumbhash', async () => {
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
+18 -2
View File
@@ -77,7 +77,7 @@ export class MediaService {
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> { async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination) ? this.assetRepository.getAll(pagination, { isVisible: true })
: this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL); : this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
}); });
@@ -178,6 +178,10 @@ export class MediaService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!asset.isVisible) {
return JobStatus.SKIPPED;
}
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat); const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
await this.assetRepository.update({ id: asset.id, previewPath }); await this.assetRepository.update({ id: asset.id, previewPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
@@ -230,6 +234,10 @@ export class MediaService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!asset.isVisible) {
return JobStatus.SKIPPED;
}
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
await this.assetRepository.update({ id: asset.id, thumbnailPath }); await this.assetRepository.update({ id: asset.id, thumbnailPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
@@ -237,7 +245,15 @@ export class MediaService {
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> { async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id]);
if (!asset?.previewPath) { if (!asset) {
return JobStatus.FAILED;
}
if (!asset.isVisible) {
return JobStatus.SKIPPED;
}
if (!asset.previewPath) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
+11 -2
View File
@@ -292,7 +292,12 @@ export class PersonService {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true }) ? this.assetRepository.getAll(pagination, {
orderDirection: 'DESC',
withFaces: true,
withArchived: true,
isVisible: true,
})
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES); : this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
}); });
@@ -322,6 +327,10 @@ export class PersonService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!asset.isVisible) {
return JobStatus.SKIPPED;
}
const faces = await this.machineLearningRepository.detectFaces( const faces = await this.machineLearningRepository.detectFaces(
machineLearning.url, machineLearning.url,
{ imagePath: asset.previewPath }, { imagePath: asset.previewPath },
@@ -424,7 +433,7 @@ export class PersonService {
this.logger.debug(`Face ${id} has ${matches.length} matches`); this.logger.debug(`Face ${id} has ${matches.length} matches`);
const isCore = matches.length >= machineLearning.facialRecognition.minFaces; const isCore = matches.length >= machineLearning.facialRecognition.minFaces && !face.asset.isArchived;
if (!isCore && !deferred) { if (!isCore && !deferred) {
this.logger.debug(`Deferring non-core face ${id} for later processing`); this.logger.debug(`Deferring non-core face ${id} for later processing`);
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } }); await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });
@@ -0,0 +1,77 @@
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { SessionService } from 'src/services/session.service';
import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
import { Mocked } from 'vitest';
describe('SessionService', () => {
let sut: SessionService;
let accessMock: Mocked<IAccessRepositoryMock>;
let loggerMock: Mocked<ILoggerRepository>;
let sessionMock: Mocked<ISessionRepository>;
beforeEach(() => {
accessMock = newAccessRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sessionMock = newSessionRepositoryMock();
sut = new SessionService(accessMock, loggerMock, sessionMock);
});
it('should be defined', () => {
expect(sut).toBeDefined();
});
describe('getAll', () => {
it('should get the devices', async () => {
sessionMock.getByUserId.mockResolvedValue([sessionStub.valid, sessionStub.inactive]);
await expect(sut.getAll(authStub.user1)).resolves.toEqual([
{
createdAt: '2021-01-01T00:00:00.000Z',
current: true,
deviceOS: '',
deviceType: '',
id: 'token-id',
updatedAt: expect.any(String),
},
{
createdAt: '2021-01-01T00:00:00.000Z',
current: false,
deviceOS: 'Android',
deviceType: 'Mobile',
id: 'not_active',
updatedAt: expect.any(String),
},
]);
expect(sessionMock.getByUserId).toHaveBeenCalledWith(authStub.user1.user.id);
});
});
describe('logoutDevices', () => {
it('should logout all devices', async () => {
sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid]);
await sut.deleteAll(authStub.user1);
expect(sessionMock.getByUserId).toHaveBeenCalledWith(authStub.user1.user.id);
expect(sessionMock.delete).toHaveBeenCalledWith('not_active');
expect(sessionMock.delete).not.toHaveBeenCalledWith('token-id');
});
});
describe('logoutDevice', () => {
it('should logout the device', async () => {
accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1']));
await sut.delete(authStub.user1, 'token-1');
expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1']));
expect(sessionMock.delete).toHaveBeenCalledWith('token-1');
});
});
});
+41
View File
@@ -0,0 +1,41 @@
import { Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
@Injectable()
export class SessionService {
private access: AccessCore;
constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
) {
this.logger.setContext(SessionService.name);
this.access = AccessCore.create(accessRepository);
}
async getAll(auth: AuthDto): Promise<SessionResponseDto[]> {
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
return sessions.map((session) => mapSession(session, auth.session?.id));
}
async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id);
await this.sessionRepository.delete(id);
}
async deleteAll(auth: AuthDto): Promise<void> {
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
for (const session of sessions) {
if (session.id === auth.session?.id) {
continue;
}
await this.sessionRepository.delete(session.id);
}
}
}
+17 -15
View File
@@ -1,8 +1,7 @@
import { AssetEntity } from 'src/entities/asset.entity';
import { SystemConfigKey } from 'src/entities/system-config.entity'; import { SystemConfigKey } from 'src/entities/system-config.entity';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISearchRepository } from 'src/interfaces/search.interface';
@@ -19,11 +18,6 @@ import { newSearchRepositoryMock } from 'test/repositories/search.repository.moc
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
const asset = {
id: 'asset-1',
previewPath: 'path/to/resize.ext',
} as AssetEntity;
describe(SmartInfoService.name, () => { describe(SmartInfoService.name, () => {
let sut: SmartInfoService; let sut: SmartInfoService;
let assetMock: Mocked<IAssetRepository>; let assetMock: Mocked<IAssetRepository>;
@@ -44,7 +38,7 @@ describe(SmartInfoService.name, () => {
loggerMock = newLoggerRepositoryMock(); loggerMock = newLoggerRepositoryMock();
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock); sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock);
assetMock.getByIds.mockResolvedValue([asset]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
}); });
it('should work', () => { it('should work', () => {
@@ -92,17 +86,16 @@ describe(SmartInfoService.name, () => {
it('should do nothing if machine learning is disabled', async () => { it('should do nothing if machine learning is disabled', async () => {
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]); configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
await sut.handleEncodeClip({ id: '123' }); expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED);
expect(assetMock.getByIds).not.toHaveBeenCalled(); expect(assetMock.getByIds).not.toHaveBeenCalled();
expect(machineMock.encodeImage).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled();
}); });
it('should skip assets without a resize path', async () => { it('should skip assets without a resize path', async () => {
const asset = { previewPath: '' } as AssetEntity; assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
assetMock.getByIds.mockResolvedValue([asset]);
await sut.handleEncodeClip({ id: asset.id }); expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED);
expect(searchMock.upsert).not.toHaveBeenCalled(); expect(searchMock.upsert).not.toHaveBeenCalled();
expect(machineMock.encodeImage).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled();
@@ -111,14 +104,23 @@ describe(SmartInfoService.name, () => {
it('should save the returned objects', async () => { it('should save the returned objects', async () => {
machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
await sut.handleEncodeClip({ id: asset.id }); expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
expect(machineMock.encodeImage).toHaveBeenCalledWith( expect(machineMock.encodeImage).toHaveBeenCalledWith(
'http://immich-machine-learning:3003', 'http://immich-machine-learning:3003',
{ imagePath: 'path/to/resize.ext' }, { imagePath: assetStub.image.previewPath },
{ enabled: true, modelName: 'ViT-B-32__openai' }, { enabled: true, modelName: 'ViT-B-32__openai' },
); );
expect(searchMock.upsert).toHaveBeenCalledWith('asset-1', [0.01, 0.02, 0.03]); expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]);
});
it('should skip invisible assets', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
expect(machineMock.encodeImage).not.toHaveBeenCalled();
expect(searchMock.upsert).not.toHaveBeenCalled();
}); });
}); });
+5 -1
View File
@@ -60,7 +60,7 @@ export class SmartInfoService {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination) ? this.assetRepository.getAll(pagination, { isVisible: true })
: this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH); : this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH);
}); });
@@ -84,6 +84,10 @@ export class SmartInfoService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!asset.isVisible) {
return JobStatus.SKIPPED;
}
if (!asset.previewPath) { if (!asset.previewPath) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
+5
View File
@@ -37,6 +37,11 @@ export class UserService {
this.configCore = SystemConfigCore.create(configRepository, this.logger); this.configCore = SystemConfigCore.create(configRepository, this.logger);
} }
async listUsers(): Promise<UserResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: true });
return users.map((user) => mapUser(user));
}
async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> { async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: !isAll }); const users = await this.userRepository.getList({ withDeleted: !isAll });
return users.map((user) => mapUser(user)); return users.map((user) => mapUser(user));
+7 -7
View File
@@ -1,6 +1,6 @@
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SessionEntity } from 'src/entities/session.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
export const adminSignupStub = { export const adminSignupStub = {
@@ -35,9 +35,9 @@ export const authStub = {
email: 'immich@test.com', email: 'immich@test.com',
isAdmin: false, isAdmin: false,
} as UserEntity, } as UserEntity,
userToken: { session: {
id: 'token-id', id: 'token-id',
} as UserTokenEntity, } as SessionEntity,
}), }),
user2: Object.freeze<AuthDto>({ user2: Object.freeze<AuthDto>({
user: { user: {
@@ -45,9 +45,9 @@ export const authStub = {
email: 'user2@immich.app', email: 'user2@immich.app',
isAdmin: false, isAdmin: false,
} as UserEntity, } as UserEntity,
userToken: { session: {
id: 'token-id', id: 'token-id',
} as UserTokenEntity, } as SessionEntity,
}), }),
external1: Object.freeze<AuthDto>({ external1: Object.freeze<AuthDto>({
user: { user: {
@@ -55,9 +55,9 @@ export const authStub = {
email: 'immich@test.com', email: 'immich@test.com',
isAdmin: false, isAdmin: false,
} as UserEntity, } as UserEntity,
userToken: { session: {
id: 'token-id', id: 'token-id',
} as UserTokenEntity, } as SessionEntity,
}), }),
adminSharedLink: Object.freeze<AuthDto>({ adminSharedLink: Object.freeze<AuthDto>({
user: { user: {
@@ -1,8 +1,8 @@
import { UserTokenEntity } from 'src/entities/user-token.entity'; import { SessionEntity } from 'src/entities/session.entity';
import { userStub } from 'test/fixtures/user.stub'; import { userStub } from 'test/fixtures/user.stub';
export const userTokenStub = { export const sessionStub = {
userToken: Object.freeze<UserTokenEntity>({ valid: Object.freeze<SessionEntity>({
id: 'token-id', id: 'token-id',
token: 'auth_token', token: 'auth_token',
userId: userStub.user1.id, userId: userStub.user1.id,
@@ -12,7 +12,7 @@ export const userTokenStub = {
deviceType: '', deviceType: '',
deviceOS: '', deviceOS: '',
}), }),
inactiveToken: Object.freeze<UserTokenEntity>({ inactive: Object.freeze<SessionEntity>({
id: 'not_active', id: 'not_active',
token: 'auth_token', token: 'auth_token',
userId: userStub.user1.id, userId: userStub.user1.id,
@@ -0,0 +1,12 @@
import { ISessionRepository } from 'src/interfaces/session.interface';
import { Mocked, vitest } from 'vitest';
export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {
return {
create: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
getByToken: vitest.fn(),
getByUserId: vitest.fn(),
};
};
@@ -1,12 +0,0 @@
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
import { Mocked, vitest } from 'vitest';
export const newUserTokenRepositoryMock = (): Mocked<IUserTokenRepository> => {
return {
create: vitest.fn(),
save: vitest.fn(),
delete: vitest.fn(),
getByToken: vitest.fn(),
getAll: vitest.fn(),
};
};
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e FROM node:iron-alpine3.18@sha256:d328c7bc3305e1ab26491817936c8151a47a8861ad617c16c1eeaa9c8075c8f6
RUN apk add --no-cache tini RUN apk add --no-cache tini
USER node USER node
@@ -12,7 +12,7 @@
import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils'; import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile } from '$lib/utils/asset-utils'; import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile, unstackAssets } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { shortcuts } from '$lib/utils/shortcut'; import { shortcuts } from '$lib/utils/shortcut';
import { SlideshowHistory } from '$lib/utils/slideshow-history'; import { SlideshowHistory } from '$lib/utils/slideshow-history';
@@ -28,7 +28,6 @@
getAllAlbums, getAllAlbums,
runAssetJobs, runAssetJobs,
updateAsset, updateAsset,
updateAssets,
updateAlbumInfo, updateAlbumInfo,
type ActivityResponseDto, type ActivityResponseDto,
type AlbumResponseDto, type AlbumResponseDto,
@@ -481,20 +480,15 @@
}; };
const handleUnstack = async () => { const handleUnstack = async () => {
try { const unstackedAssets = await unstackAssets($stackAssetsStore);
const ids = $stackAssetsStore.map(({ id }) => id); if (unstackedAssets) {
await updateAssets({ assetBulkUpdateDto: { ids, removeParent: true } }); for (const asset of unstackedAssets) {
for (const child of $stackAssetsStore) { dispatch('action', {
child.stackParentId = null; type: AssetAction.ADD,
child.stackCount = 0; asset,
child.stack = []; });
dispatch('action', { type: AssetAction.ADD, asset: child });
} }
dispatch('close'); dispatch('close');
notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 });
} catch (error) {
handleError(error, `Unable to unstack`);
} }
}; };
@@ -14,6 +14,9 @@
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { getAltText } from '$lib/utils/thumbnail-util'; import { getAltText } from '$lib/utils/thumbnail-util';
import { slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
const { slideshowState, slideshowLook } = slideshowStore;
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let preloadAssets: AssetResponseDto[] | null = null; export let preloadAssets: AssetResponseDto[] | null = null;
@@ -158,7 +161,9 @@
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }} transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
src={assetData} src={assetData}
alt={getAltText(asset)} alt={getAltText(asset)}
class="h-full w-full object-contain" class="h-full w-full {$slideshowState === SlideshowState.None
? 'object-contain'
: slideshowLookCssMapping[$slideshowLook]}"
draggable="false" draggable="false"
/> />
{#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewer) as boundingbox} {#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewer) as boundingbox}
@@ -21,7 +21,7 @@
export let peopleWithFaces: AssetFaceResponseDto[]; export let peopleWithFaces: AssetFaceResponseDto[];
export let allPeople: PersonResponseDto[]; export let allPeople: PersonResponseDto[];
export let editedPersonIndex: number; export let editedPerson: PersonResponseDto;
export let assetType: AssetTypeEnum; export let assetType: AssetTypeEnum;
export let assetId: string; export let assetId: string;
@@ -106,7 +106,7 @@
const handleCreatePerson = async () => { const handleCreatePerson = async () => {
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner); const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id); const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
const newFeaturePhoto = personToUpdate ? await zoomImageToBase64(personToUpdate) : null; const newFeaturePhoto = personToUpdate ? await zoomImageToBase64(personToUpdate) : null;
@@ -229,7 +229,7 @@
<div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto"> <div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
{#if searchName == ''} {#if searchName == ''}
{#each allPeople as person (person.id)} {#each allPeople as person (person.id)}
{#if person.id !== peopleWithFaces[editedPersonIndex].person?.id} {#if person.id !== editedPerson.id}
<div class="w-fit"> <div class="w-fit">
<button class="w-[90px]" on:click={() => dispatch('reassign', person)}> <button class="w-[90px]" on:click={() => dispatch('reassign', person)}>
<div class="relative"> <div class="relative">
@@ -255,7 +255,7 @@
{/each} {/each}
{:else} {:else}
{#each searchedPeople as person (person.id)} {#each searchedPeople as person (person.id)}
{#if person.id !== peopleWithFaces[editedPersonIndex].person?.id} {#if person.id !== editedPerson.id}
<div class="w-fit"> <div class="w-fit">
<button class="w-[90px]" on:click={() => dispatch('reassign', person)}> <button class="w-[90px]" on:click={() => dispatch('reassign', person)}>
<div class="relative"> <div class="relative">
@@ -28,14 +28,14 @@
export let assetType: AssetTypeEnum; export let assetType: AssetTypeEnum;
// keep track of the changes // keep track of the changes
let numberOfPersonToCreate: string[] = []; let peopleToCreate: string[] = [];
let numberOfAssetFaceGenerated: string[] = []; let assetFaceGenerated: string[] = [];
// faces // faces
let peopleWithFaces: AssetFaceResponseDto[] = []; let peopleWithFaces: AssetFaceResponseDto[] = [];
let selectedPersonToReassign: (PersonResponseDto | null)[]; let selectedPersonToReassign: Record<string, PersonResponseDto> = {};
let selectedPersonToCreate: (string | null)[]; let selectedPersonToCreate: Record<string, string> = {};
let editedPersonIndex: number; let editedPerson: PersonResponseDto;
// loading spinners // loading spinners
let isShowLoadingDone = false; let isShowLoadingDone = false;
@@ -49,6 +49,8 @@
let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>; let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>;
let automaticRefreshTimeout: ReturnType<typeof setTimeout>; let automaticRefreshTimeout: ReturnType<typeof setTimeout>;
const thumbnailWidth = '90px';
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
close: void; close: void;
refresh: void; refresh: void;
@@ -60,8 +62,6 @@
const { people } = await getAllPeople({ withHidden: true }); const { people } = await getAllPeople({ withHidden: true });
allPeople = people; allPeople = people;
peopleWithFaces = await getFaces({ id: assetId }); peopleWithFaces = await getFaces({ id: assetId });
selectedPersonToCreate = Array.from({ length: peopleWithFaces.length });
selectedPersonToReassign = Array.from({ length: peopleWithFaces.length });
} catch (error) { } catch (error) {
handleError(error, "Can't get faces"); handleError(error, "Can't get faces");
} finally { } finally {
@@ -71,12 +71,12 @@
} }
const onPersonThumbnail = (personId: string) => { const onPersonThumbnail = (personId: string) => {
numberOfAssetFaceGenerated.push(personId); assetFaceGenerated.push(personId);
if ( if (
isEqual(numberOfAssetFaceGenerated, numberOfPersonToCreate) && isEqual(assetFaceGenerated, peopleToCreate) &&
loaderLoadingDoneTimeout && loaderLoadingDoneTimeout &&
automaticRefreshTimeout && automaticRefreshTimeout &&
selectedPersonToCreate.filter((person) => person !== null).length === numberOfPersonToCreate.length Object.keys(selectedPersonToCreate).length === peopleToCreate.length
) { ) {
clearTimeout(loaderLoadingDoneTimeout); clearTimeout(loaderLoadingDoneTimeout);
clearTimeout(automaticRefreshTimeout); clearTimeout(automaticRefreshTimeout);
@@ -97,36 +97,41 @@
dispatch('close'); dispatch('close');
}; };
const handleReset = (index: number) => { const handleReset = (id: string) => {
if (selectedPersonToReassign[index]) { if (selectedPersonToReassign[id]) {
selectedPersonToReassign[index] = null; delete selectedPersonToReassign[id];
// trigger reactivity
selectedPersonToReassign = selectedPersonToReassign;
} }
if (selectedPersonToCreate[index]) { if (selectedPersonToCreate[id]) {
selectedPersonToCreate[index] = null; delete selectedPersonToCreate[id];
// trigger reactivity
selectedPersonToCreate = selectedPersonToCreate;
} }
}; };
const handleEditFaces = async () => { const handleEditFaces = async () => {
loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), timeBeforeShowLoadingSpinner); loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), timeBeforeShowLoadingSpinner);
const numberOfChanges = const numberOfChanges = Object.keys(selectedPersonToCreate).length + Object.keys(selectedPersonToReassign).length;
selectedPersonToCreate.filter((person) => person !== null).length +
selectedPersonToReassign.filter((person) => person !== null).length;
if (numberOfChanges > 0) { if (numberOfChanges > 0) {
try { try {
for (const [index, peopleWithFace] of peopleWithFaces.entries()) { for (const personWithFace of peopleWithFaces) {
const personId = selectedPersonToReassign[index]?.id; const personId = selectedPersonToReassign[personWithFace.id]?.id;
if (personId) { if (personId) {
await reassignFacesById({ await reassignFacesById({
id: personId, id: personId,
faceDto: { id: peopleWithFace.id }, faceDto: { id: personWithFace.id },
}); });
} else if (selectedPersonToCreate[index]) { } else if (selectedPersonToCreate[personWithFace.id]) {
const data = await createPerson({ personCreateDto: {} }); const data = await createPerson({ personCreateDto: {} });
numberOfPersonToCreate.push(data.id); peopleToCreate.push(data.id);
await reassignFacesById({ await reassignFacesById({
id: data.id, id: data.id,
faceDto: { id: peopleWithFace.id }, faceDto: { id: personWithFace.id },
}); });
} }
} }
@@ -141,7 +146,7 @@
} }
isShowLoadingDone = false; isShowLoadingDone = false;
if (numberOfPersonToCreate.length === 0) { if (peopleToCreate.length === 0) {
clearTimeout(loaderLoadingDoneTimeout); clearTimeout(loaderLoadingDoneTimeout);
dispatch('refresh'); dispatch('refresh');
} else { } else {
@@ -150,23 +155,26 @@
}; };
const handleCreatePerson = (newFeaturePhoto: string | null) => { const handleCreatePerson = (newFeaturePhoto: string | null) => {
const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id); const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
if (newFeaturePhoto && personToUpdate) { if (newFeaturePhoto && personToUpdate) {
selectedPersonToCreate[peopleWithFaces.indexOf(personToUpdate)] = newFeaturePhoto; selectedPersonToCreate[personToUpdate.id] = newFeaturePhoto;
} }
showSeletecFaces = false; showSeletecFaces = false;
}; };
const handleReassignFace = (person: PersonResponseDto | null) => { const handleReassignFace = (person: PersonResponseDto | null) => {
if (person) { const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
selectedPersonToReassign[editedPersonIndex] = person; if (person && personToUpdate) {
selectedPersonToReassign[personToUpdate.id] = person;
showSeletecFaces = false; showSeletecFaces = false;
} }
}; };
const handlePersonPicker = (index: number) => { const handlePersonPicker = (person: PersonResponseDto | null) => {
editedPersonIndex = index; if (person) {
showSeletecFaces = true; editedPerson = person;
showSeletecFaces = true;
}
}; };
</script> </script>
@@ -217,35 +225,48 @@
on:mouseleave={() => ($boundingBoxesArray = [])} on:mouseleave={() => ($boundingBoxesArray = [])}
> >
<div class="relative"> <div class="relative">
<ImageThumbnail {#if selectedPersonToCreate[face.id]}
curve <ImageThumbnail
shadow curve
url={selectedPersonToCreate[index] || shadow
getPeopleThumbnailUrl(selectedPersonToReassign[index]?.id || face.person.id)} url={selectedPersonToCreate[face.id]}
altText={selectedPersonToReassign[index] altText={selectedPersonToCreate[face.id]}
? selectedPersonToReassign[index]?.name title={'New person'}
: selectedPersonToCreate[index] widthStyle={thumbnailWidth}
? 'New person' heightStyle={thumbnailWidth}
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)} />
title={selectedPersonToReassign[index] {:else if selectedPersonToReassign[face.id]}
? selectedPersonToReassign[index]?.name <ImageThumbnail
: selectedPersonToCreate[index] curve
? 'New person' shadow
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)} url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id].id)}
widthStyle="90px" altText={selectedPersonToReassign[face.id]?.name || selectedPersonToReassign[face.id].id}
heightStyle="90px" title={getPersonNameWithHiddenValue(
thumbhash={null} selectedPersonToReassign[face.id].name,
hidden={selectedPersonToReassign[index] face.person?.isHidden,
? selectedPersonToReassign[index]?.isHidden )}
: selectedPersonToCreate[index] widthStyle={thumbnailWidth}
? false heightStyle={thumbnailWidth}
: face.person?.isHidden} hidden={selectedPersonToReassign[face.id].isHidden}
/> />
{:else}
<ImageThumbnail
curve
shadow
url={getPeopleThumbnailUrl(face.person.id)}
altText={face.person.name || face.person.id}
title={getPersonNameWithHiddenValue(face.person.name, face.person.isHidden)}
widthStyle={thumbnailWidth}
heightStyle={thumbnailWidth}
hidden={face.person.isHidden}
/>
{/if}
</div> </div>
{#if !selectedPersonToCreate[index]}
{#if !selectedPersonToCreate[face.id]}
<p class="relative mt-1 truncate font-medium" title={face.person?.name}> <p class="relative mt-1 truncate font-medium" title={face.person?.name}>
{#if selectedPersonToReassign[index]?.id} {#if selectedPersonToReassign[face.id]?.id}
{selectedPersonToReassign[index]?.name} {selectedPersonToReassign[face.id]?.name}
{:else} {:else}
{face.person?.name} {face.person?.name}
{/if} {/if}
@@ -253,8 +274,8 @@
{/if} {/if}
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700"> <div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700">
{#if selectedPersonToCreate[index] || selectedPersonToReassign[index]} {#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
<button on:click={() => handleReset(index)} class="flex h-full w-full"> <button on:click={() => handleReset(face.id)} class="flex h-full w-full">
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"> <div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<div> <div>
<Icon path={mdiRestart} size={18} /> <Icon path={mdiRestart} size={18} />
@@ -262,7 +283,7 @@
</div> </div>
</button> </button>
{:else} {:else}
<button on:click={() => handlePersonPicker(index)} class="flex h-full w-full"> <button on:click={() => handlePersonPicker(face.person)} class="flex h-full w-full">
<div <div
class="absolute left-1/2 top-1/2 h-[2px] w-[14px] translate-x-[-50%] translate-y-[-50%] transform bg-white" class="absolute left-1/2 top-1/2 h-[2px] w-[14px] translate-x-[-50%] translate-y-[-50%] transform bg-white"
/> />
@@ -282,7 +303,7 @@
<AssignFaceSidePanel <AssignFaceSidePanel
{peopleWithFaces} {peopleWithFaces}
{allPeople} {allPeople}
{editedPersonIndex} {editedPerson}
{assetType} {assetType}
{assetId} {assetId}
on:close={() => (showSeletecFaces = false)} on:close={() => (showSeletecFaces = false)}
@@ -1,20 +1,45 @@
<script lang="ts"> <script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
import type { OnStack } from '$lib/utils/actions'; import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
import { stackAssets } from '$lib/utils/asset-utils'; import { stackAssets, unstackAssets } from '$lib/utils/asset-utils';
import { mdiImageMultipleOutline } from '@mdi/js'; import type { OnStack, OnUnstack } from '$lib/utils/actions';
export let unstack = false;
export let onStack: OnStack | undefined; export let onStack: OnStack | undefined;
export let onUnstack: OnUnstack | undefined;
const { clearSelect, getOwnedAssets } = getAssetControlContext(); const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleStack = async () => { const handleStack = async () => {
await stackAssets([...getOwnedAssets()], (ids) => { const selectedAssets = [...getOwnedAssets()];
const ids = await stackAssets(selectedAssets);
if (ids) {
onStack?.(ids); onStack?.(ids);
clearSelect(); clearSelect();
}); }
};
const handleUnstack = async () => {
const selectedAssets = [...getOwnedAssets()];
if (selectedAssets.length !== 1) {
return;
}
const { stack } = selectedAssets[0];
if (!stack) {
return;
}
const assets = [selectedAssets[0], ...stack];
const unstackedAssets = await unstackAssets(assets);
if (unstackedAssets) {
onUnstack?.(unstackedAssets);
}
clearSelect();
}; };
</script> </script>
<MenuOption text="Stack" icon={mdiImageMultipleOutline} on:click={handleStack} /> {#if unstack}
<MenuOption text="Un-stack" icon={mdiImageMinusOutline} on:click={handleUnstack} />
{:else}
<MenuOption text="Stack" icon={mdiImageMultipleOutline} on:click={handleStack} />
{/if}
@@ -89,11 +89,10 @@
}; };
const onStackAssets = async () => { const onStackAssets = async () => {
if ($selectedAssets.size > 1) { const ids = await stackAssets(Array.from($selectedAssets));
await stackAssets(Array.from($selectedAssets), (ids) => { if (ids) {
assetStore.removeAssets(ids); assetStore.removeAssets(ids);
dispatch('escape'); dispatch('escape');
});
} }
}; };
@@ -107,6 +106,8 @@
{ shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteractionStore) }, { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteractionStore) },
{ shortcut: { key: 'PageUp' }, onShortcut: () => (element.scrollTop = 0) },
{ shortcut: { key: 'PageDown' }, onShortcut: () => (element.scrollTop = viewport.height) },
]; ];
if ($isMultiSelectState) { if ($isMultiSelectState) {
@@ -4,27 +4,34 @@
SettingInputFieldType, SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte'; } from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { mdiArrowDownThin, mdiArrowUpThin, mdiShuffle } from '@mdi/js'; import { mdiArrowDownThin, mdiArrowUpThin, mdiFitToPageOutline, mdiFitToScreenOutline, mdiShuffle } from '@mdi/js';
import { SlideshowNavigation, slideshowStore } from '../stores/slideshow.store'; import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
import Button from './elements/buttons/button.svelte'; import Button from './elements/buttons/button.svelte';
import type { RenderedOption } from './elements/dropdown.svelte'; import type { RenderedOption } from './elements/dropdown.svelte';
import SettingDropdown from './shared-components/settings/setting-dropdown.svelte'; import SettingDropdown from './shared-components/settings/setting-dropdown.svelte';
const { slideshowDelay, showProgressBar, slideshowNavigation } = slideshowStore; const { slideshowDelay, showProgressBar, slideshowNavigation, slideshowLook } = slideshowStore;
export let onClose = () => {}; export let onClose = () => {};
const options: Record<SlideshowNavigation, RenderedOption> = { const navigationOptions: Record<SlideshowNavigation, RenderedOption> = {
[SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: 'Shuffle' }, [SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: 'Shuffle' },
[SlideshowNavigation.AscendingOrder]: { icon: mdiArrowUpThin, title: 'Backward' }, [SlideshowNavigation.AscendingOrder]: { icon: mdiArrowUpThin, title: 'Backward' },
[SlideshowNavigation.DescendingOrder]: { icon: mdiArrowDownThin, title: 'Forward' }, [SlideshowNavigation.DescendingOrder]: { icon: mdiArrowDownThin, title: 'Forward' },
}; };
const handleToggle = (selectedOption: RenderedOption) => { const lookOptions: Record<SlideshowLook, RenderedOption> = {
[SlideshowLook.Contain]: { icon: mdiFitToScreenOutline, title: 'Contain' },
[SlideshowLook.Cover]: { icon: mdiFitToPageOutline, title: 'Cover' },
};
const handleToggle = <Type extends SlideshowNavigation | SlideshowLook>(
record: RenderedOption,
options: Record<Type, RenderedOption>,
): undefined | Type => {
for (const [key, option] of Object.entries(options)) { for (const [key, option] of Object.entries(options)) {
if (option === selectedOption) { if (option === record) {
$slideshowNavigation = key as SlideshowNavigation; return key as Type;
break;
} }
} }
}; };
@@ -34,9 +41,19 @@
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"> <div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown <SettingDropdown
title="Direction" title="Direction"
options={Object.values(options)} options={Object.values(navigationOptions)}
selectedOption={options[$slideshowNavigation]} selectedOption={navigationOptions[$slideshowNavigation]}
onToggle={(option) => handleToggle(option)} onToggle={(option) => {
$slideshowNavigation = handleToggle(option, navigationOptions) || $slideshowNavigation;
}}
/>
<SettingDropdown
title="Look"
options={Object.values(lookOptions)}
selectedOption={lookOptions[$slideshowLook]}
onToggle={(option) => {
$slideshowLook = handleToggle(option, lookOptions) || $slideshowLook;
}}
/> />
<SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} /> <SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} />
<SettingInputField <SettingInputField
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import type { AuthDeviceResponseDto } from '@immich/sdk'; import type { SessionResponseDto } from '@immich/sdk';
import { import {
mdiAndroid, mdiAndroid,
mdiApple, mdiApple,
@@ -15,7 +15,7 @@
import { DateTime, type ToRelativeCalendarOptions } from 'luxon'; import { DateTime, type ToRelativeCalendarOptions } from 'luxon';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let device: AuthDeviceResponseDto; export let device: SessionResponseDto;
const dispatcher = createEventDispatcher<{ const dispatcher = createEventDispatcher<{
delete: void; delete: void;
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import { getAuthDevices, logoutAuthDevice, logoutAuthDevices, type AuthDeviceResponseDto } from '@immich/sdk'; import { deleteAllSessions, deleteSession, getSessions, type SessionResponseDto } from '@immich/sdk';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { notificationController, NotificationType } from '../shared-components/notification/notification';
import DeviceCard from './device-card.svelte'; import DeviceCard from './device-card.svelte';
export let devices: AuthDeviceResponseDto[]; export let devices: SessionResponseDto[];
let deleteDevice: AuthDeviceResponseDto | null = null; let deleteDevice: SessionResponseDto | null = null;
let deleteAll = false; let deleteAll = false;
const refresh = () => getAuthDevices().then((_devices) => (devices = _devices)); const refresh = () => getSessions().then((_devices) => (devices = _devices));
$: currentDevice = devices.find((device) => device.current); $: currentDevice = devices.find((device) => device.current);
$: otherDevices = devices.filter((device) => !device.current); $: otherDevices = devices.filter((device) => !device.current);
@@ -21,7 +21,7 @@
} }
try { try {
await logoutAuthDevice({ id: deleteDevice.id }); await deleteSession({ id: deleteDevice.id });
notificationController.show({ message: `Logged out device`, type: NotificationType.Info }); notificationController.show({ message: `Logged out device`, type: NotificationType.Info });
} catch (error) { } catch (error) {
handleError(error, 'Unable to log out device'); handleError(error, 'Unable to log out device');
@@ -33,7 +33,7 @@
const handleDeleteAll = async () => { const handleDeleteAll = async () => {
try { try {
await logoutAuthDevices(); await deleteAllSessions();
notificationController.show({ notificationController.show({
message: `Logged out all devices`, message: `Logged out all devices`,
type: NotificationType.Info, type: NotificationType.Info,
@@ -4,7 +4,8 @@
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { oauth } from '$lib/utils'; import { oauth } from '$lib/utils';
import { type ApiKeyResponseDto, type AuthDeviceResponseDto } from '@immich/sdk'; import { type ApiKeyResponseDto, type SessionResponseDto } from '@immich/sdk';
import SettingAccordionState from '../shared-components/settings/setting-accordion-state.svelte';
import SettingAccordion from '../shared-components/settings/setting-accordion.svelte'; import SettingAccordion from '../shared-components/settings/setting-accordion.svelte';
import AppSettings from './app-settings.svelte'; import AppSettings from './app-settings.svelte';
import ChangePasswordSettings from './change-password-settings.svelte'; import ChangePasswordSettings from './change-password-settings.svelte';
@@ -14,10 +15,9 @@
import PartnerSettings from './partner-settings.svelte'; import PartnerSettings from './partner-settings.svelte';
import UserAPIKeyList from './user-api-key-list.svelte'; import UserAPIKeyList from './user-api-key-list.svelte';
import UserProfileSettings from './user-profile-settings.svelte'; import UserProfileSettings from './user-profile-settings.svelte';
import SettingAccordionState from '../shared-components/settings/setting-accordion-state.svelte';
export let keys: ApiKeyResponseDto[] = []; export let keys: ApiKeyResponseDto[] = [];
export let devices: AuthDeviceResponseDto[] = []; export let sessions: SessionResponseDto[] = [];
let oauthOpen = let oauthOpen =
oauth.isCallback(window.location) || oauth.isCallback(window.location) ||
@@ -38,7 +38,7 @@
</SettingAccordion> </SettingAccordion>
<SettingAccordion key="authorized-devices" title="Authorized Devices" subtitle="Manage your logged-in devices"> <SettingAccordion key="authorized-devices" title="Authorized Devices" subtitle="Manage your logged-in devices">
<DeviceList bind:devices /> <DeviceList bind:devices={sessions} />
</SettingAccordion> </SettingAccordion>
<SettingAccordion key="memories" title="Memories" subtitle="Manage what you see in your memories"> <SettingAccordion key="memories" title="Memories" subtitle="Manage what you see in your memories">
+12
View File
@@ -13,6 +13,16 @@ export enum SlideshowNavigation {
DescendingOrder = 'descending-order', DescendingOrder = 'descending-order',
} }
export enum SlideshowLook {
Contain = 'contain',
Cover = 'cover',
}
export const slideshowLookCssMapping: Record<SlideshowLook, string> = {
[SlideshowLook.Contain]: 'object-contain',
[SlideshowLook.Cover]: 'object-cover',
};
function createSlideshowStore() { function createSlideshowStore() {
const restartState = writable<boolean>(false); const restartState = writable<boolean>(false);
const stopState = writable<boolean>(false); const stopState = writable<boolean>(false);
@@ -21,6 +31,7 @@ function createSlideshowStore() {
'slideshow-navigation', 'slideshow-navigation',
SlideshowNavigation.DescendingOrder, SlideshowNavigation.DescendingOrder,
); );
const slideshowLook = persisted<SlideshowLook>('slideshow-look', SlideshowLook.Contain);
const slideshowState = writable<SlideshowState>(SlideshowState.None); const slideshowState = writable<SlideshowState>(SlideshowState.None);
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true); const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
@@ -50,6 +61,7 @@ function createSlideshowStore() {
}, },
}, },
slideshowNavigation, slideshowNavigation,
slideshowLook,
slideshowState, slideshowState,
slideshowDelay, slideshowDelay,
showProgressBar, showProgressBar,
+2 -1
View File
@@ -1,5 +1,5 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import { deleteAssets as deleteBulk } from '@immich/sdk'; import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
export type OnDelete = (assetIds: string[]) => void; export type OnDelete = (assetIds: string[]) => void;
@@ -7,6 +7,7 @@ export type OnRestore = (ids: string[]) => void;
export type OnArchive = (ids: string[], isArchived: boolean) => void; export type OnArchive = (ids: string[], isArchived: boolean) => void;
export type OnFavorite = (ids: string[], favorite: boolean) => void; export type OnFavorite = (ids: string[], favorite: boolean) => void;
export type OnStack = (ids: string[]) => void; export type OnStack = (ids: string[]) => void;
export type OnUnstack = (assets: AssetResponseDto[]) => void;
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => { export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
try { try {
+69 -30
View File
@@ -2,6 +2,7 @@ import { goto } from '$app/navigation';
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification'; import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
import { downloadManager } from '$lib/stores/download'; import { downloadManager } from '$lib/stores/download';
import { downloadRequest, getKey } from '$lib/utils'; import { downloadRequest, getKey } from '$lib/utils';
@@ -269,43 +270,81 @@ export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserRespo
return ids; return ids;
}; };
export async function stackAssets(assets: Array<AssetResponseDto>, onStack: (ds: string[]) => void) { export const stackAssets = async (assets: AssetResponseDto[]) => {
if (assets.length < 2) {
return false;
}
const parent = assets[0];
const children = assets.slice(1);
const ids = children.map(({ id }) => id);
try { try {
const parent = assets.at(0); await updateAssets({
if (!parent) { assetBulkUpdateDto: {
return; ids,
} stackParentId: parent.id,
},
});
} catch (error) {
handleError(error, 'Failed to stack assets');
return false;
}
const children = assets.slice(1); let grandChildren: AssetResponseDto[] = [];
const ids = children.map(({ id }) => id); for (const asset of children) {
asset.stackParentId = parent.id;
if (children.length > 0) { if (asset.stack) {
await updateAssets({ assetBulkUpdateDto: { ids, stackParentId: parent.id } }); // Add grand-children to new parent
} grandChildren = grandChildren.concat(asset.stack);
let childrenCount = parent.stackCount || 1;
for (const asset of children) {
asset.stackParentId = parent.id;
// Add grand-children's count to new parent
childrenCount += asset.stackCount || 1;
// Reset children stack info // Reset children stack info
asset.stackCount = null; asset.stackCount = null;
asset.stack = []; asset.stack = [];
} }
parent.stackCount = childrenCount;
notificationController.show({
message: `Stacked ${ids.length + 1} assets`,
type: NotificationType.Info,
timeout: 1500,
});
onStack(ids);
} catch (error) {
handleError(error, `Unable to stack`);
} }
}
parent.stack ??= [];
parent.stack = parent.stack.concat(children, grandChildren);
parent.stackCount = parent.stack.length + 1;
notificationController.show({
message: `Stacked ${parent.stackCount} assets`,
type: NotificationType.Info,
button: {
text: 'View Stack',
onClick() {
return assetViewingStore.setAssetId(parent.id);
},
},
});
return ids;
};
export const unstackAssets = async (assets: AssetResponseDto[]) => {
const ids = assets.map(({ id }) => id);
try {
await updateAssets({
assetBulkUpdateDto: {
ids,
removeParent: true,
},
});
} catch (error) {
handleError(error, 'Failed to un-stack assets');
return;
}
for (const asset of assets) {
asset.stackParentId = null;
asset.stackCount = null;
asset.stack = [];
}
notificationController.show({
type: NotificationType.Info,
message: `Un-stacked ${assets.length} assets`,
});
return assets;
};
export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => {
if (get(isSelectingAllAssets)) { if (get(isSelectingAllAssets)) {
+14 -3
View File
@@ -30,7 +30,14 @@
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
$: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite); let isAllFavorite: boolean;
let isAssetStackSelected: boolean;
$: {
const selection = [...$selectedAssets];
isAllFavorite = selection.every((asset) => asset.isFavorite);
isAssetStackSelected = selection.length === 1 && !!selection[0].stack;
}
const handleEscape = () => { const handleEscape = () => {
if ($showAssetViewer) { if ($showAssetViewer) {
@@ -62,8 +69,12 @@
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} /> <FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
<AssetSelectContextMenu icon={mdiDotsVertical} title="Menu"> <AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
<DownloadAction menuItem /> <DownloadAction menuItem />
{#if $selectedAssets.size > 1} {#if $selectedAssets.size > 1 || isAssetStackSelected}
<StackAction onStack={(assetIds) => assetStore.removeAssets(assetIds)} /> <StackAction
unstack={isAssetStackSelected}
onStack={(assetIds) => assetStore.removeAssets(assetIds)}
onUnstack={(assets) => assetStore.addAssets(assets)}
/>
{/if} {/if}
<ChangeDate menuItem /> <ChangeDate menuItem />
<ChangeLocation menuItem /> <ChangeLocation menuItem />
@@ -19,7 +19,7 @@
</svelte:fragment> </svelte:fragment>
<section class="mx-4 flex place-content-center"> <section class="mx-4 flex place-content-center">
<div class="w-full max-w-3xl"> <div class="w-full max-w-3xl">
<UserSettingsList keys={data.keys} devices={data.devices} /> <UserSettingsList keys={data.keys} sessions={data.sessions} />
</div> </div>
</section> </section>
</UserPageLayout> </UserPageLayout>
+3 -3
View File
@@ -1,16 +1,16 @@
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { getApiKeys, getAuthDevices } from '@immich/sdk'; import { getApiKeys, getSessions } from '@immich/sdk';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async () => { export const load = (async () => {
await authenticate(); await authenticate();
const keys = await getApiKeys(); const keys = await getApiKeys();
const devices = await getAuthDevices(); const sessions = await getSessions();
return { return {
keys, keys,
devices, sessions,
meta: { meta: {
title: 'Settings', title: 'Settings',
}, },