mirror of
https://github.com/immich-app/immich.git
synced 2026-02-11 19:38:54 +03:00
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:
@@ -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
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
||||
|
||||
database:
|
||||
container_name: immich_postgres
|
||||
|
||||
@@ -54,7 +54,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
||||
restart: always
|
||||
|
||||
database:
|
||||
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
|
||||
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
|
||||
|
||||
database:
|
||||
|
||||
@@ -52,8 +52,8 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
||||
|
||||
Hostname
|
||||
|
||||
- `https://immich.example.com/auth/login`)
|
||||
- `https://immich.example.com/user-settings`)
|
||||
- `https://immich.example.com/auth/login`
|
||||
- `https://immich.example.com/user-settings`
|
||||
|
||||
## Enable OAuth
|
||||
|
||||
|
||||
@@ -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
|
||||
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).
|
||||
|
||||
@@ -36,7 +36,7 @@ services:
|
||||
<<: *server-common
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
||||
|
||||
database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
|
||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
||||
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
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', () => {
|
||||
it('should reject an invalid token', async () => {
|
||||
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
|
||||
|
||||
75
e2e/src/api/specs/session.e2e-spec.ts
Normal file
75
e2e/src/api/specs/session.e2e-spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
19
e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts
Normal file
19
e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts
Normal file
@@ -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'");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,7 +44,7 @@ import { loginDto, signupDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
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 WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number };
|
||||
type AdminSetupOptions = { onboarding?: boolean };
|
||||
@@ -60,13 +60,15 @@ export const testAssetDirInternal = '/data/assets';
|
||||
export const tempDir = tmpdir();
|
||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||
export const immichCli = async (args: string[]) => {
|
||||
let _resolve: (value: CliResponse) => void;
|
||||
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
|
||||
const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args];
|
||||
const child = spawn('node', _args, {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
export const immichCli = (args: string[]) =>
|
||||
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]);
|
||||
export const immichAdmin = (args: string[]) =>
|
||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||
|
||||
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 stderr = '';
|
||||
@@ -139,7 +141,7 @@ export const utils = {
|
||||
'asset_faces',
|
||||
'activity',
|
||||
'api_keys',
|
||||
'user_token',
|
||||
'sessions',
|
||||
'users',
|
||||
'system_metadata',
|
||||
'system_config',
|
||||
|
||||
@@ -10,7 +10,7 @@ try {
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
||||
include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
|
||||
globalSetup,
|
||||
testTimeout: 15_000,
|
||||
poolOptions: {
|
||||
|
||||
11
machine-learning/poetry.lock
generated
11
machine-learning/poetry.lock
generated
@@ -1150,22 +1150,23 @@ test = ["objgraph", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "21.2.0"
|
||||
version = "22.0.0"
|
||||
description = "WSGI HTTP Server for UNIX"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"},
|
||||
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"},
|
||||
{file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"},
|
||||
{file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = "*"
|
||||
|
||||
[package.extras]
|
||||
eventlet = ["eventlet (>=0.24.1)"]
|
||||
eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
|
||||
gevent = ["gevent (>=1.4.0)"]
|
||||
setproctitle = ["setproctitle"]
|
||||
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
|
||||
tornado = ["tornado (>=0.2)"]
|
||||
|
||||
[[package]]
|
||||
|
||||
Binary file not shown.
@@ -39,27 +39,24 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
buildTopRow() {
|
||||
return Row(
|
||||
return Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => context.pop(),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: InkWell(
|
||||
onTap: () => context.pop(),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'IMMICH',
|
||||
style: TextStyle(
|
||||
fontFamily: 'SnowburstOne',
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.primaryColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
context.isDarkTheme
|
||||
? 'assets/immich-text-dark.png'
|
||||
: 'assets/immich-text-light.png',
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
18
mobile/openapi/.openapi-generator/FILES
generated
18
mobile/openapi/.openapi-generator/FILES
generated
@@ -16,8 +16,6 @@ doc/AddUsersDto.md
|
||||
doc/AlbumApi.md
|
||||
doc/AlbumCountResponseDto.md
|
||||
doc/AlbumResponseDto.md
|
||||
doc/AlbumUserResponseDto.md
|
||||
doc/AlbumUserRole.md
|
||||
doc/AllJobStatusResponseDto.md
|
||||
doc/AssetApi.md
|
||||
doc/AssetBulkDeleteDto.md
|
||||
@@ -43,7 +41,6 @@ doc/AssetTypeEnum.md
|
||||
doc/AudioCodec.md
|
||||
doc/AuditApi.md
|
||||
doc/AuditDeletesResponseDto.md
|
||||
doc/AuthDeviceResponseDto.md
|
||||
doc/AuthenticationApi.md
|
||||
doc/BulkIdResponseDto.md
|
||||
doc/BulkIdsDto.md
|
||||
@@ -144,6 +141,8 @@ doc/ServerPingResponse.md
|
||||
doc/ServerStatsResponseDto.md
|
||||
doc/ServerThemeDto.md
|
||||
doc/ServerVersionResponseDto.md
|
||||
doc/SessionResponseDto.md
|
||||
doc/SessionsApi.md
|
||||
doc/SharedLinkApi.md
|
||||
doc/SharedLinkCreateDto.md
|
||||
doc/SharedLinkEditDto.md
|
||||
@@ -186,7 +185,6 @@ doc/TranscodeHWAccel.md
|
||||
doc/TranscodePolicy.md
|
||||
doc/TrashApi.md
|
||||
doc/UpdateAlbumDto.md
|
||||
doc/UpdateAlbumUserDto.md
|
||||
doc/UpdateAssetDto.md
|
||||
doc/UpdateLibraryDto.md
|
||||
doc/UpdatePartnerDto.md
|
||||
@@ -222,6 +220,7 @@ lib/api/partner_api.dart
|
||||
lib/api/person_api.dart
|
||||
lib/api/search_api.dart
|
||||
lib/api/server_info_api.dart
|
||||
lib/api/sessions_api.dart
|
||||
lib/api/shared_link_api.dart
|
||||
lib/api/sync_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/album_count_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/api_key_create_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/audio_codec.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_ids_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_theme_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_edit_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_policy.dart
|
||||
lib/model/update_album_dto.dart
|
||||
lib/model/update_album_user_dto.dart
|
||||
lib/model/update_asset_dto.dart
|
||||
lib/model/update_library_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_count_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/api_key_api_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/audit_api_test.dart
|
||||
test/audit_deletes_response_dto_test.dart
|
||||
test/auth_device_response_dto_test.dart
|
||||
test/authentication_api_test.dart
|
||||
test/bulk_id_response_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_theme_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_create_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/trash_api_test.dart
|
||||
test/update_album_dto_test.dart
|
||||
test/update_album_user_dto_test.dart
|
||||
test/update_asset_dto_test.dart
|
||||
test/update_library_dto_test.dart
|
||||
test/update_partner_dto_test.dart
|
||||
|
||||
12
mobile/openapi/README.md
generated
12
mobile/openapi/README.md
generated
@@ -91,7 +91,6 @@ Class | Method | HTTP request | Description
|
||||
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{id}/assets |
|
||||
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{id}/user/{userId} |
|
||||
*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* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||
*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* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
|
||||
*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* | [**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* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||
*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* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||
*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* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
|
||||
*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
|
||||
@@ -236,8 +235,6 @@ Class | Method | HTTP request | Description
|
||||
- [AddUsersDto](doc//AddUsersDto.md)
|
||||
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
|
||||
- [AlbumResponseDto](doc//AlbumResponseDto.md)
|
||||
- [AlbumUserResponseDto](doc//AlbumUserResponseDto.md)
|
||||
- [AlbumUserRole](doc//AlbumUserRole.md)
|
||||
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
|
||||
- [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
|
||||
- [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
|
||||
@@ -261,7 +258,6 @@ Class | Method | HTTP request | Description
|
||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||
- [AudioCodec](doc//AudioCodec.md)
|
||||
- [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md)
|
||||
- [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md)
|
||||
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
||||
- [BulkIdsDto](doc//BulkIdsDto.md)
|
||||
- [CLIPConfig](doc//CLIPConfig.md)
|
||||
@@ -351,6 +347,7 @@ Class | Method | HTTP request | Description
|
||||
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
||||
- [ServerThemeDto](doc//ServerThemeDto.md)
|
||||
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||
@@ -387,7 +384,6 @@ Class | Method | HTTP request | Description
|
||||
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
|
||||
- [TranscodePolicy](doc//TranscodePolicy.md)
|
||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
|
||||
- [UpdatePartnerDto](doc//UpdatePartnerDto.md)
|
||||
|
||||
158
mobile/openapi/doc/AuthenticationApi.md
generated
158
mobile/openapi/doc/AuthenticationApi.md
generated
@@ -10,11 +10,8 @@ All URIs are relative to */api*
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||
[**getAuthDevices**](AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
|
||||
[**login**](AuthenticationApi.md#login) | **POST** /auth/login |
|
||||
[**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 |
|
||||
[**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)
|
||||
|
||||
# **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**
|
||||
> 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)
|
||||
|
||||
# **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**
|
||||
> UserResponseDto signUpAdmin(signUpDto)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# openapi.model.AuthDeviceResponseDto
|
||||
# openapi.model.SessionResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
171
mobile/openapi/doc/SessionsApi.md
generated
Normal file
171
mobile/openapi/doc/SessionsApi.md
generated
Normal 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)
|
||||
|
||||
6
mobile/openapi/lib/api.dart
generated
6
mobile/openapi/lib/api.dart
generated
@@ -45,6 +45,7 @@ part 'api/partner_api.dart';
|
||||
part 'api/person_api.dart';
|
||||
part 'api/search_api.dart';
|
||||
part 'api/server_info_api.dart';
|
||||
part 'api/sessions_api.dart';
|
||||
part 'api/shared_link_api.dart';
|
||||
part 'api/sync_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/album_count_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/asset_bulk_delete_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/audio_codec.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_ids_dto.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_theme_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_edit_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_policy.dart';
|
||||
part 'model/update_album_dto.dart';
|
||||
part 'model/update_album_user_dto.dart';
|
||||
part 'model/update_asset_dto.dart';
|
||||
part 'model/update_library_dto.dart';
|
||||
part 'model/update_partner_dto.dart';
|
||||
|
||||
117
mobile/openapi/lib/api/authentication_api.dart
generated
117
mobile/openapi/lib/api/authentication_api.dart
generated
@@ -63,50 +63,6 @@ class AuthenticationApi {
|
||||
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].
|
||||
/// Parameters:
|
||||
///
|
||||
@@ -195,79 +151,6 @@ class AuthenticationApi {
|
||||
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].
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
135
mobile/openapi/lib/api/sessions_api.dart
generated
Normal file
135
mobile/openapi/lib/api/sessions_api.dart
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
10
mobile/openapi/lib/api_client.dart
generated
10
mobile/openapi/lib/api_client.dart
generated
@@ -202,10 +202,6 @@ class ApiClient {
|
||||
return AlbumCountResponseDto.fromJson(value);
|
||||
case 'AlbumResponseDto':
|
||||
return AlbumResponseDto.fromJson(value);
|
||||
case 'AlbumUserResponseDto':
|
||||
return AlbumUserResponseDto.fromJson(value);
|
||||
case 'AlbumUserRole':
|
||||
return AlbumUserRoleTypeTransformer().decode(value);
|
||||
case 'AllJobStatusResponseDto':
|
||||
return AllJobStatusResponseDto.fromJson(value);
|
||||
case 'AssetBulkDeleteDto':
|
||||
@@ -252,8 +248,6 @@ class ApiClient {
|
||||
return AudioCodecTypeTransformer().decode(value);
|
||||
case 'AuditDeletesResponseDto':
|
||||
return AuditDeletesResponseDto.fromJson(value);
|
||||
case 'AuthDeviceResponseDto':
|
||||
return AuthDeviceResponseDto.fromJson(value);
|
||||
case 'BulkIdResponseDto':
|
||||
return BulkIdResponseDto.fromJson(value);
|
||||
case 'BulkIdsDto':
|
||||
@@ -432,6 +426,8 @@ class ApiClient {
|
||||
return ServerThemeDto.fromJson(value);
|
||||
case 'ServerVersionResponseDto':
|
||||
return ServerVersionResponseDto.fromJson(value);
|
||||
case 'SessionResponseDto':
|
||||
return SessionResponseDto.fromJson(value);
|
||||
case 'SharedLinkCreateDto':
|
||||
return SharedLinkCreateDto.fromJson(value);
|
||||
case 'SharedLinkEditDto':
|
||||
@@ -504,8 +500,6 @@ class ApiClient {
|
||||
return TranscodePolicyTypeTransformer().decode(value);
|
||||
case 'UpdateAlbumDto':
|
||||
return UpdateAlbumDto.fromJson(value);
|
||||
case 'UpdateAlbumUserDto':
|
||||
return UpdateAlbumUserDto.fromJson(value);
|
||||
case 'UpdateAssetDto':
|
||||
return UpdateAssetDto.fromJson(value);
|
||||
case 'UpdateLibraryDto':
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AuthDeviceResponseDto {
|
||||
/// Returns a new [AuthDeviceResponseDto] instance.
|
||||
AuthDeviceResponseDto({
|
||||
class SessionResponseDto {
|
||||
/// Returns a new [SessionResponseDto] instance.
|
||||
SessionResponseDto({
|
||||
required this.createdAt,
|
||||
required this.current,
|
||||
required this.deviceOS,
|
||||
@@ -34,7 +34,7 @@ class AuthDeviceResponseDto {
|
||||
String updatedAt;
|
||||
|
||||
@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.current == current &&
|
||||
other.deviceOS == deviceOS &&
|
||||
@@ -53,7 +53,7 @@ class AuthDeviceResponseDto {
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@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() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -66,14 +66,14 @@ class AuthDeviceResponseDto {
|
||||
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.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AuthDeviceResponseDto? fromJson(dynamic value) {
|
||||
static SessionResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AuthDeviceResponseDto(
|
||||
return SessionResponseDto(
|
||||
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
||||
current: mapValueOfType<bool>(json, r'current')!,
|
||||
deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
|
||||
@@ -85,11 +85,11 @@ class AuthDeviceResponseDto {
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AuthDeviceResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AuthDeviceResponseDto>[];
|
||||
static List<SessionResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SessionResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AuthDeviceResponseDto.fromJson(row);
|
||||
final value = SessionResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@@ -98,12 +98,12 @@ class AuthDeviceResponseDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AuthDeviceResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AuthDeviceResponseDto>{};
|
||||
static Map<String, SessionResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SessionResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AuthDeviceResponseDto.fromJson(entry.value);
|
||||
final value = SessionResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@@ -112,14 +112,14 @@ class AuthDeviceResponseDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AuthDeviceResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AuthDeviceResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AuthDeviceResponseDto>>{};
|
||||
// maps a json object with a list of SessionResponseDto-objects as value to a dart map
|
||||
static Map<String, List<SessionResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SessionResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
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;
|
||||
15
mobile/openapi/test/authentication_api_test.dart
generated
15
mobile/openapi/test/authentication_api_test.dart
generated
@@ -22,11 +22,6 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<AuthDeviceResponseDto>> getAuthDevices() async
|
||||
test('test getAuthDevices', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<LoginResponseDto> login(LoginCredentialDto loginCredentialDto) async
|
||||
test('test login', () async {
|
||||
// TODO
|
||||
@@ -37,16 +32,6 @@ void main() {
|
||||
// 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
|
||||
test('test signUpAdmin', () async {
|
||||
// TODO
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AuthDeviceResponseDto
|
||||
// tests for SessionResponseDto
|
||||
void main() {
|
||||
// final instance = AuthDeviceResponseDto();
|
||||
// final instance = SessionResponseDto();
|
||||
|
||||
group('test AuthDeviceResponseDto', () {
|
||||
group('test SessionResponseDto', () {
|
||||
// String createdAt
|
||||
test('to test the property `createdAt`', () async {
|
||||
// TODO
|
||||
36
mobile/openapi/test/sessions_api_test.dart
generated
Normal file
36
mobile/openapi/test/sessions_api_test.dart
generated
Normal 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
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
@@ -105,9 +105,6 @@ flutter:
|
||||
- assets/
|
||||
- assets/i18n/
|
||||
fonts:
|
||||
- family: SnowburstOne
|
||||
fonts:
|
||||
- asset: fonts/SnowburstOne.ttf
|
||||
- family: Inconsolata
|
||||
fonts:
|
||||
- asset: fonts/Inconsolata-Regular.ttf
|
||||
|
||||
@@ -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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"get": {
|
||||
"operationId": "getAllSharedLinks",
|
||||
@@ -7976,37 +7976,6 @@
|
||||
],
|
||||
"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": {
|
||||
"properties": {
|
||||
"error": {
|
||||
@@ -10133,6 +10102,37 @@
|
||||
],
|
||||
"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": {
|
||||
"properties": {
|
||||
"albumId": {
|
||||
|
||||
@@ -355,14 +355,6 @@ export type ChangePasswordDto = {
|
||||
newPassword: string;
|
||||
password: string;
|
||||
};
|
||||
export type AuthDeviceResponseDto = {
|
||||
createdAt: string;
|
||||
current: boolean;
|
||||
deviceOS: string;
|
||||
deviceType: string;
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
export type LoginCredentialDto = {
|
||||
email: string;
|
||||
password: string;
|
||||
@@ -800,6 +792,14 @@ export type ServerVersionResponseDto = {
|
||||
minor: number;
|
||||
patch: number;
|
||||
};
|
||||
export type SessionResponseDto = {
|
||||
createdAt: string;
|
||||
current: boolean;
|
||||
deviceOS: string;
|
||||
deviceType: string;
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
export type SharedLinkResponseDto = {
|
||||
album?: AlbumResponseDto;
|
||||
allowDownload: boolean;
|
||||
@@ -1723,28 +1723,6 @@ export function changePassword({ 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 }: {
|
||||
loginCredentialDto: LoginCredentialDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@@ -2433,6 +2411,28 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) {
|
||||
...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) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
|
||||
@Command({
|
||||
@@ -13,16 +12,7 @@ export class ListUsersCommand extends CommandRunner {
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
const users = await this.userService.getAll(
|
||||
{
|
||||
user: {
|
||||
id: 'cli',
|
||||
email: 'cli@immich.app',
|
||||
isAdmin: true,
|
||||
} as UserEntity,
|
||||
},
|
||||
true,
|
||||
);
|
||||
const users = await this.userService.listUsers();
|
||||
console.dir(users);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -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 { Request, Response } from 'express';
|
||||
import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants';
|
||||
import {
|
||||
AuthDeviceResponseDto,
|
||||
AuthDto,
|
||||
ChangePasswordDto,
|
||||
LoginCredentialDto,
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Authentication')
|
||||
@Controller('auth')
|
||||
@@ -41,23 +39,6 @@ export class AuthController {
|
||||
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')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
validateAccessToken(): ValidateAccessTokenResponseDto {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { PartnerController } from 'src/controllers/partner.controller';
|
||||
import { PersonController } from 'src/controllers/person.controller';
|
||||
import { SearchController } from 'src/controllers/search.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 { SyncController } from 'src/controllers/sync.controller';
|
||||
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
||||
@@ -43,6 +44,7 @@ export const controllers = [
|
||||
PartnerController,
|
||||
SearchController,
|
||||
ServerInfoController,
|
||||
SessionController,
|
||||
SharedLinkController,
|
||||
SyncController,
|
||||
SystemConfigController,
|
||||
|
||||
31
server/src/controllers/session.controller.ts
Normal file
31
server/src/controllers/session.controller.ts
Normal file
@@ -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,8 +2,8 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export class AuthDto {
|
||||
@@ -11,7 +11,7 @@ export class AuthDto {
|
||||
|
||||
apiKey?: APIKeyEntity;
|
||||
sharedLink?: SharedLinkEntity;
|
||||
userToken?: UserTokenEntity;
|
||||
session?: SessionEntity;
|
||||
}
|
||||
|
||||
export class LoginCredentialDto {
|
||||
@@ -78,24 +78,6 @@ export class ValidateAccessTokenResponseDto {
|
||||
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 {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
|
||||
19
server/src/dtos/session.dto.ts
Normal file
19
server/src/dtos/session.dto.ts
Normal 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,
|
||||
});
|
||||
@@ -14,13 +14,13 @@ import { MemoryEntity } from 'src/entities/memory.entity';
|
||||
import { MoveEntity } from 'src/entities/move.entity';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
||||
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export const entities = [
|
||||
@@ -46,6 +46,6 @@ export const entities = [
|
||||
SystemMetadataEntity,
|
||||
TagEntity,
|
||||
UserEntity,
|
||||
UserTokenEntity,
|
||||
SessionEntity,
|
||||
LibraryEntity,
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('user_token')
|
||||
export class UserTokenEntity {
|
||||
@Entity('sessions')
|
||||
export class SessionEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
11
server/src/interfaces/session.interface.ts
Normal file
11
server/src/interfaces/session.interface.ts
Normal file
@@ -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[]>;
|
||||
}
|
||||
15
server/src/migrations/1713490844785-RenameSessionsTable.ts
Normal file
15
server/src/migrations/1713490844785-RenameSessionsTable.ts
Normal file
@@ -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"`);
|
||||
}
|
||||
}
|
||||
@@ -185,13 +185,13 @@ WHERE
|
||||
|
||||
-- AccessRepository.authDevice.checkOwnerAccess
|
||||
SELECT
|
||||
"UserTokenEntity"."id" AS "UserTokenEntity_id"
|
||||
"SessionEntity"."id" AS "SessionEntity_id"
|
||||
FROM
|
||||
"user_token" "UserTokenEntity"
|
||||
"sessions" "SessionEntity"
|
||||
WHERE
|
||||
(
|
||||
("UserTokenEntity"."userId" = $1)
|
||||
AND ("UserTokenEntity"."id" IN ($2))
|
||||
("SessionEntity"."userId" = $1)
|
||||
AND ("SessionEntity"."id" IN ($2))
|
||||
)
|
||||
|
||||
-- AccessRepository.library.checkOwnerAccess
|
||||
|
||||
48
server/src/queries/session.repository.sql
Normal file
48
server/src/queries/session.repository.sql
Normal 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
|
||||
@@ -10,8 +10,8 @@ import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { MemoryEntity } from 'src/entities/memory.entity';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SessionEntity } from 'src/entities/session.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 { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { Brackets, In, Repository } from 'typeorm';
|
||||
@@ -293,7 +293,7 @@ class AssetAccess implements IAssetAccess {
|
||||
}
|
||||
|
||||
class AuthDeviceAccess implements IAuthDeviceAccess {
|
||||
constructor(private tokenRepository: Repository<UserTokenEntity>) {}
|
||||
constructor(private sessionRepository: Repository<SessionEntity>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
@@ -302,7 +302,7 @@ class AuthDeviceAccess implements IAuthDeviceAccess {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return this.tokenRepository
|
||||
return this.sessionRepository
|
||||
.find({
|
||||
select: { id: true },
|
||||
where: {
|
||||
@@ -464,12 +464,12 @@ export class AccessRepository implements IAccessRepository {
|
||||
@InjectRepository(PersonEntity) personRepository: Repository<PersonEntity>,
|
||||
@InjectRepository(AssetFaceEntity) assetFaceRepository: Repository<AssetFaceEntity>,
|
||||
@InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository<SharedLinkEntity>,
|
||||
@InjectRepository(UserTokenEntity) tokenRepository: Repository<UserTokenEntity>,
|
||||
@InjectRepository(SessionEntity) sessionRepository: Repository<SessionEntity>,
|
||||
) {
|
||||
this.activity = new ActivityAccess(activityRepository, albumRepository);
|
||||
this.album = new AlbumAccess(albumRepository, 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.memory = new MemoryAccess(memoryRepository);
|
||||
this.person = new PersonAccess(assetFaceRepository, personRepository);
|
||||
|
||||
@@ -23,12 +23,12 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { ISearchRepository } from 'src/interfaces/search.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 { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AccessRepository } from 'src/repositories/access.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 { SearchRepository } from 'src/repositories/search.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 { StorageRepository } from 'src/repositories/storage.repository';
|
||||
import { SystemConfigRepository } from 'src/repositories/system-config.repository';
|
||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||
import { TagRepository } from 'src/repositories/tag.repository';
|
||||
import { UserTokenRepository } from 'src/repositories/user-token.repository';
|
||||
import { UserRepository } from 'src/repositories/user.repository';
|
||||
|
||||
export const repositories = [
|
||||
@@ -89,11 +89,11 @@ export const repositories = [
|
||||
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||
{ provide: ISessionRepository, useClass: SessionRepository },
|
||||
{ provide: IStorageRepository, useClass: StorageRepository },
|
||||
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||
{ provide: ITagRepository, useClass: TagRepository },
|
||||
{ provide: IMediaRepository, useClass: MediaRepository },
|
||||
{ provide: IUserRepository, useClass: UserRepository },
|
||||
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
|
||||
];
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
||||
import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
@Instrumentation()
|
||||
@Injectable()
|
||||
export class UserTokenRepository implements IUserTokenRepository {
|
||||
constructor(@InjectRepository(UserTokenEntity) private repository: Repository<UserTokenEntity>) {}
|
||||
export class SessionRepository implements ISessionRepository {
|
||||
constructor(@InjectRepository(SessionEntity) private repository: Repository<SessionEntity>) {}
|
||||
|
||||
@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 } });
|
||||
}
|
||||
|
||||
getAll(userId: string): Promise<UserTokenEntity[]> {
|
||||
getByUserId(userId: string): Promise<SessionEntity[]> {
|
||||
return this.repository.find({
|
||||
where: {
|
||||
userId,
|
||||
@@ -31,12 +31,12 @@ export class UserTokenRepository implements IUserTokenRepository {
|
||||
});
|
||||
}
|
||||
|
||||
create(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> {
|
||||
return this.repository.save(userToken);
|
||||
create(session: Partial<SessionEntity>): Promise<SessionEntity> {
|
||||
return this.repository.save(session);
|
||||
}
|
||||
|
||||
save(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> {
|
||||
return this.repository.save(userToken);
|
||||
update(session: Partial<SessionEntity>): Promise<SessionEntity> {
|
||||
return this.repository.save(session);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
@@ -9,25 +9,25 @@ import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.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 { AuthService } from 'src/services/auth.service';
|
||||
import { keyStub } from 'test/fixtures/api-key.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 { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userTokenStub } from 'test/fixtures/user-token.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||
import { newLibraryRepositoryMock } from 'test/repositories/library.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 { 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 { Mock, Mocked, vitest } from 'vitest';
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('AuthService', () => {
|
||||
let libraryMock: Mocked<ILibraryRepository>;
|
||||
let loggerMock: Mocked<ILoggerRepository>;
|
||||
let configMock: Mocked<ISystemConfigRepository>;
|
||||
let userTokenMock: Mocked<IUserTokenRepository>;
|
||||
let sessionMock: Mocked<ISessionRepository>;
|
||||
let shareMock: Mocked<ISharedLinkRepository>;
|
||||
let keyMock: Mocked<IKeyRepository>;
|
||||
|
||||
@@ -98,7 +98,7 @@ describe('AuthService', () => {
|
||||
libraryMock = newLibraryRepositoryMock();
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
userTokenMock = newUserTokenRepositoryMock();
|
||||
sessionMock = newSessionRepositoryMock();
|
||||
shareMock = newSharedLinkRepositoryMock();
|
||||
keyMock = newKeyRepositoryMock();
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('AuthService', () => {
|
||||
libraryMock,
|
||||
loggerMock,
|
||||
userMock,
|
||||
userTokenMock,
|
||||
sessionMock,
|
||||
shareMock,
|
||||
keyMock,
|
||||
);
|
||||
@@ -139,14 +139,14 @@ describe('AuthService', () => {
|
||||
|
||||
it('should successfully log the user in', async () => {
|
||||
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);
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should generate the cookie headers (insecure)', async () => {
|
||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
await expect(
|
||||
sut.login(fixtures.login, {
|
||||
clientIp: '127.0.0.1',
|
||||
@@ -231,14 +231,14 @@ describe('AuthService', () => {
|
||||
});
|
||||
|
||||
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({
|
||||
successful: true,
|
||||
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 () => {
|
||||
@@ -282,11 +282,11 @@ describe('AuthService', () => {
|
||||
|
||||
it('should validate using authorization header', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.user1);
|
||||
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
|
||||
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
|
||||
const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
|
||||
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({
|
||||
user: userStub.user1,
|
||||
userToken: userTokenStub.userToken,
|
||||
session: sessionStub.valid,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -336,37 +336,29 @@ describe('AuthService', () => {
|
||||
|
||||
describe('validate - user token', () => {
|
||||
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' };
|
||||
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
|
||||
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' };
|
||||
await expect(sut.validate(headers, {})).resolves.toEqual({
|
||||
user: userStub.user1,
|
||||
userToken: userTokenStub.userToken,
|
||||
session: sessionStub.valid,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update when access time exceeds an hour', async () => {
|
||||
userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken);
|
||||
userTokenMock.save.mockResolvedValue(userTokenStub.userToken);
|
||||
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive);
|
||||
sessionMock.update.mockResolvedValue(sessionStub.valid);
|
||||
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
||||
await expect(sut.validate(headers, {})).resolves.toEqual({
|
||||
user: userStub.user1,
|
||||
userToken: userTokenStub.userToken,
|
||||
});
|
||||
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',
|
||||
session: sessionStub.valid,
|
||||
});
|
||||
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', () => {
|
||||
it('should pass along the query params', () => {
|
||||
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);
|
||||
userMock.getByEmail.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(
|
||||
loginResponseStub.user1oauth,
|
||||
@@ -478,7 +421,7 @@ describe('AuthService', () => {
|
||||
userMock.getByEmail.mockResolvedValue(null);
|
||||
userMock.getAdmin.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(
|
||||
loginResponseStub.user1oauth,
|
||||
@@ -491,7 +434,7 @@ describe('AuthService', () => {
|
||||
it('should use the mobile redirect override', async () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.override);
|
||||
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
||||
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
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 () => {
|
||||
configMock.load.mockResolvedValue(systemConfigStub.override);
|
||||
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
||||
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
|
||||
await sut.callback({ url: `app.immich:///?code=abc123` }, loginDetails);
|
||||
|
||||
|
||||
@@ -19,11 +19,10 @@ import {
|
||||
LOGIN_URL,
|
||||
MOBILE_REDIRECT,
|
||||
} 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 { UserCore } from 'src/cores/user.core';
|
||||
import {
|
||||
AuthDeviceResponseDto,
|
||||
AuthDto,
|
||||
ChangePasswordDto,
|
||||
LoginCredentialDto,
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
OAuthConfigDto,
|
||||
SignUpDto,
|
||||
mapLoginResponse,
|
||||
mapUserToken,
|
||||
} from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
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 { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.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 { HumanReadableSize } from 'src/utils/bytes';
|
||||
|
||||
@@ -85,7 +83,7 @@ export class AuthService {
|
||||
@Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
@Inject(IUserTokenRepository) private userTokenRepository: IUserTokenRepository,
|
||||
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
|
||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
||||
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
||||
) {
|
||||
@@ -120,8 +118,8 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
||||
if (auth.userToken) {
|
||||
await this.userTokenRepository.delete(auth.userToken.id);
|
||||
if (auth.session) {
|
||||
await this.sessionRepository.delete(auth.session.id);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -164,8 +162,9 @@ export class AuthService {
|
||||
|
||||
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
|
||||
const shareKey = (headers['x-immich-share-key'] || params.key) as string;
|
||||
const userToken = (headers['x-immich-user-token'] ||
|
||||
params.userToken ||
|
||||
const session = (headers['x-immich-user-token'] ||
|
||||
headers['x-immich-session-token'] ||
|
||||
params.sessionKey ||
|
||||
this.getBearerToken(headers) ||
|
||||
this.getCookieToken(headers)) as string;
|
||||
const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string;
|
||||
@@ -174,8 +173,8 @@ export class AuthService {
|
||||
return this.validateSharedLink(shareKey);
|
||||
}
|
||||
|
||||
if (userToken) {
|
||||
return this.validateUserToken(userToken);
|
||||
if (session) {
|
||||
return this.validateSession(session);
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
@@ -185,26 +184,6 @@ export class AuthService {
|
||||
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) {
|
||||
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
|
||||
}
|
||||
@@ -408,19 +387,19 @@ export class AuthService {
|
||||
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);
|
||||
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 updatedAt = DateTime.fromJSDate(userToken.updatedAt);
|
||||
const updatedAt = DateTime.fromJSDate(session.updatedAt);
|
||||
const diff = now.diff(updatedAt, ['hours']);
|
||||
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');
|
||||
@@ -430,7 +409,7 @@ export class AuthService {
|
||||
const key = this.cryptoRepository.newPassword(32);
|
||||
const token = this.cryptoRepository.hashSha256(key);
|
||||
|
||||
await this.userTokenRepository.create({
|
||||
await this.sessionRepository.create({
|
||||
token,
|
||||
user,
|
||||
deviceOS: loginDetails.deviceOS,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { PartnerService } from 'src/services/partner.service';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { SearchService } from 'src/services/search.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 { SmartInfoService } from 'src/services/smart-info.service';
|
||||
import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||
@@ -50,6 +51,7 @@ export const services = [
|
||||
PersonService,
|
||||
SearchService,
|
||||
ServerInfoService,
|
||||
SessionService,
|
||||
SharedLinkService,
|
||||
SmartInfoService,
|
||||
StorageService,
|
||||
|
||||
@@ -225,6 +225,15 @@ describe(MediaService.name, () => {
|
||||
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) => {
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
@@ -353,6 +362,15 @@ describe(MediaService.name, () => {
|
||||
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))(
|
||||
'should generate a %s thumbnail for an image when specified',
|
||||
async (format) => {
|
||||
@@ -410,6 +428,15 @@ describe(MediaService.name, () => {
|
||||
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 () => {
|
||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
|
||||
@@ -77,7 +77,7 @@ export class MediaService {
|
||||
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
|
||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||
return force
|
||||
? this.assetRepository.getAll(pagination)
|
||||
? this.assetRepository.getAll(pagination, { isVisible: true })
|
||||
: this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
|
||||
});
|
||||
|
||||
@@ -178,6 +178,10 @@ export class MediaService {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
if (!asset.isVisible) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
|
||||
await this.assetRepository.update({ id: asset.id, previewPath });
|
||||
return JobStatus.SUCCESS;
|
||||
@@ -230,6 +234,10 @@ export class MediaService {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
if (!asset.isVisible) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
|
||||
await this.assetRepository.update({ id: asset.id, thumbnailPath });
|
||||
return JobStatus.SUCCESS;
|
||||
@@ -237,7 +245,15 @@ export class MediaService {
|
||||
|
||||
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -292,7 +292,12 @@ export class PersonService {
|
||||
|
||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -322,6 +327,10 @@ export class PersonService {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
if (!asset.isVisible) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const faces = await this.machineLearningRepository.detectFaces(
|
||||
machineLearning.url,
|
||||
{ imagePath: asset.previewPath },
|
||||
@@ -424,7 +433,7 @@ export class PersonService {
|
||||
|
||||
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) {
|
||||
this.logger.debug(`Deferring non-core face ${id} for later processing`);
|
||||
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });
|
||||
|
||||
77
server/src/services/session.service.spec.ts
Normal file
77
server/src/services/session.service.spec.ts
Normal file
@@ -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
server/src/services/session.service.ts
Normal file
41
server/src/services/session.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.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 { IMachineLearningRepository } from 'src/interfaces/machine-learning.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 { Mocked } from 'vitest';
|
||||
|
||||
const asset = {
|
||||
id: 'asset-1',
|
||||
previewPath: 'path/to/resize.ext',
|
||||
} as AssetEntity;
|
||||
|
||||
describe(SmartInfoService.name, () => {
|
||||
let sut: SmartInfoService;
|
||||
let assetMock: Mocked<IAssetRepository>;
|
||||
@@ -44,7 +38,7 @@ describe(SmartInfoService.name, () => {
|
||||
loggerMock = newLoggerRepositoryMock();
|
||||
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock);
|
||||
|
||||
assetMock.getByIds.mockResolvedValue([asset]);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
@@ -92,17 +86,16 @@ describe(SmartInfoService.name, () => {
|
||||
it('should do nothing if machine learning is disabled', async () => {
|
||||
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(machineMock.encodeImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip assets without a resize path', async () => {
|
||||
const asset = { previewPath: '' } as AssetEntity;
|
||||
assetMock.getByIds.mockResolvedValue([asset]);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
|
||||
|
||||
await sut.handleEncodeClip({ id: asset.id });
|
||||
expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED);
|
||||
|
||||
expect(searchMock.upsert).not.toHaveBeenCalled();
|
||||
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
||||
@@ -111,14 +104,23 @@ describe(SmartInfoService.name, () => {
|
||||
it('should save the returned objects', async () => {
|
||||
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(
|
||||
'http://immich-machine-learning:3003',
|
||||
{ imagePath: 'path/to/resize.ext' },
|
||||
{ imagePath: assetStub.image.previewPath },
|
||||
{ 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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export class SmartInfoService {
|
||||
|
||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||
return force
|
||||
? this.assetRepository.getAll(pagination)
|
||||
? this.assetRepository.getAll(pagination, { isVisible: true })
|
||||
: this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH);
|
||||
});
|
||||
|
||||
@@ -84,6 +84,10 @@ export class SmartInfoService {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
if (!asset.isVisible) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
if (!asset.previewPath) {
|
||||
return JobStatus.FAILED;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ export class UserService {
|
||||
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[]> {
|
||||
const users = await this.userRepository.getList({ withDeleted: !isAll });
|
||||
return users.map((user) => mapUser(user));
|
||||
|
||||
14
server/test/fixtures/auth.stub.ts
vendored
14
server/test/fixtures/auth.stub.ts
vendored
@@ -1,6 +1,6 @@
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export const adminSignupStub = {
|
||||
@@ -35,9 +35,9 @@ export const authStub = {
|
||||
email: 'immich@test.com',
|
||||
isAdmin: false,
|
||||
} as UserEntity,
|
||||
userToken: {
|
||||
session: {
|
||||
id: 'token-id',
|
||||
} as UserTokenEntity,
|
||||
} as SessionEntity,
|
||||
}),
|
||||
user2: Object.freeze<AuthDto>({
|
||||
user: {
|
||||
@@ -45,9 +45,9 @@ export const authStub = {
|
||||
email: 'user2@immich.app',
|
||||
isAdmin: false,
|
||||
} as UserEntity,
|
||||
userToken: {
|
||||
session: {
|
||||
id: 'token-id',
|
||||
} as UserTokenEntity,
|
||||
} as SessionEntity,
|
||||
}),
|
||||
external1: Object.freeze<AuthDto>({
|
||||
user: {
|
||||
@@ -55,9 +55,9 @@ export const authStub = {
|
||||
email: 'immich@test.com',
|
||||
isAdmin: false,
|
||||
} as UserEntity,
|
||||
userToken: {
|
||||
session: {
|
||||
id: 'token-id',
|
||||
} as UserTokenEntity,
|
||||
} as SessionEntity,
|
||||
}),
|
||||
adminSharedLink: Object.freeze<AuthDto>({
|
||||
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';
|
||||
|
||||
export const userTokenStub = {
|
||||
userToken: Object.freeze<UserTokenEntity>({
|
||||
export const sessionStub = {
|
||||
valid: Object.freeze<SessionEntity>({
|
||||
id: 'token-id',
|
||||
token: 'auth_token',
|
||||
userId: userStub.user1.id,
|
||||
@@ -12,7 +12,7 @@ export const userTokenStub = {
|
||||
deviceType: '',
|
||||
deviceOS: '',
|
||||
}),
|
||||
inactiveToken: Object.freeze<UserTokenEntity>({
|
||||
inactive: Object.freeze<SessionEntity>({
|
||||
id: 'not_active',
|
||||
token: 'auth_token',
|
||||
userId: userStub.user1.id,
|
||||
12
server/test/repositories/session.repository.mock.ts
Normal file
12
server/test/repositories/session.repository.mock.ts
Normal file
@@ -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,4 +1,4 @@
|
||||
FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e
|
||||
FROM node:iron-alpine3.18@sha256:d328c7bc3305e1ab26491817936c8151a47a8861ad617c16c1eeaa9c8075c8f6
|
||||
|
||||
RUN apk add --no-cache tini
|
||||
USER node
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
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 { shortcuts } from '$lib/utils/shortcut';
|
||||
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
||||
@@ -28,7 +28,6 @@
|
||||
getAllAlbums,
|
||||
runAssetJobs,
|
||||
updateAsset,
|
||||
updateAssets,
|
||||
updateAlbumInfo,
|
||||
type ActivityResponseDto,
|
||||
type AlbumResponseDto,
|
||||
@@ -481,20 +480,15 @@
|
||||
};
|
||||
|
||||
const handleUnstack = async () => {
|
||||
try {
|
||||
const ids = $stackAssetsStore.map(({ id }) => id);
|
||||
await updateAssets({ assetBulkUpdateDto: { ids, removeParent: true } });
|
||||
for (const child of $stackAssetsStore) {
|
||||
child.stackParentId = null;
|
||||
child.stackCount = 0;
|
||||
child.stack = [];
|
||||
dispatch('action', { type: AssetAction.ADD, asset: child });
|
||||
const unstackedAssets = await unstackAssets($stackAssetsStore);
|
||||
if (unstackedAssets) {
|
||||
for (const asset of unstackedAssets) {
|
||||
dispatch('action', {
|
||||
type: AssetAction.ADD,
|
||||
asset,
|
||||
});
|
||||
}
|
||||
|
||||
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 { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
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 preloadAssets: AssetResponseDto[] | null = null;
|
||||
@@ -158,7 +161,9 @@
|
||||
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||
src={assetData}
|
||||
alt={getAltText(asset)}
|
||||
class="h-full w-full object-contain"
|
||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
||||
? 'object-contain'
|
||||
: slideshowLookCssMapping[$slideshowLook]}"
|
||||
draggable="false"
|
||||
/>
|
||||
{#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewer) as boundingbox}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
export let peopleWithFaces: AssetFaceResponseDto[];
|
||||
export let allPeople: PersonResponseDto[];
|
||||
export let editedPersonIndex: number;
|
||||
export let editedPerson: PersonResponseDto;
|
||||
export let assetType: AssetTypeEnum;
|
||||
export let assetId: string;
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
const handleCreatePerson = async () => {
|
||||
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;
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
<div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
|
||||
{#if searchName == ''}
|
||||
{#each allPeople as person (person.id)}
|
||||
{#if person.id !== peopleWithFaces[editedPersonIndex].person?.id}
|
||||
{#if person.id !== editedPerson.id}
|
||||
<div class="w-fit">
|
||||
<button class="w-[90px]" on:click={() => dispatch('reassign', person)}>
|
||||
<div class="relative">
|
||||
@@ -255,7 +255,7 @@
|
||||
{/each}
|
||||
{:else}
|
||||
{#each searchedPeople as person (person.id)}
|
||||
{#if person.id !== peopleWithFaces[editedPersonIndex].person?.id}
|
||||
{#if person.id !== editedPerson.id}
|
||||
<div class="w-fit">
|
||||
<button class="w-[90px]" on:click={() => dispatch('reassign', person)}>
|
||||
<div class="relative">
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
export let assetType: AssetTypeEnum;
|
||||
|
||||
// keep track of the changes
|
||||
let numberOfPersonToCreate: string[] = [];
|
||||
let numberOfAssetFaceGenerated: string[] = [];
|
||||
let peopleToCreate: string[] = [];
|
||||
let assetFaceGenerated: string[] = [];
|
||||
|
||||
// faces
|
||||
let peopleWithFaces: AssetFaceResponseDto[] = [];
|
||||
let selectedPersonToReassign: (PersonResponseDto | null)[];
|
||||
let selectedPersonToCreate: (string | null)[];
|
||||
let editedPersonIndex: number;
|
||||
let selectedPersonToReassign: Record<string, PersonResponseDto> = {};
|
||||
let selectedPersonToCreate: Record<string, string> = {};
|
||||
let editedPerson: PersonResponseDto;
|
||||
|
||||
// loading spinners
|
||||
let isShowLoadingDone = false;
|
||||
@@ -49,6 +49,8 @@
|
||||
let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>;
|
||||
let automaticRefreshTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const thumbnailWidth = '90px';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
refresh: void;
|
||||
@@ -60,8 +62,6 @@
|
||||
const { people } = await getAllPeople({ withHidden: true });
|
||||
allPeople = people;
|
||||
peopleWithFaces = await getFaces({ id: assetId });
|
||||
selectedPersonToCreate = Array.from({ length: peopleWithFaces.length });
|
||||
selectedPersonToReassign = Array.from({ length: peopleWithFaces.length });
|
||||
} catch (error) {
|
||||
handleError(error, "Can't get faces");
|
||||
} finally {
|
||||
@@ -71,12 +71,12 @@
|
||||
}
|
||||
|
||||
const onPersonThumbnail = (personId: string) => {
|
||||
numberOfAssetFaceGenerated.push(personId);
|
||||
assetFaceGenerated.push(personId);
|
||||
if (
|
||||
isEqual(numberOfAssetFaceGenerated, numberOfPersonToCreate) &&
|
||||
isEqual(assetFaceGenerated, peopleToCreate) &&
|
||||
loaderLoadingDoneTimeout &&
|
||||
automaticRefreshTimeout &&
|
||||
selectedPersonToCreate.filter((person) => person !== null).length === numberOfPersonToCreate.length
|
||||
Object.keys(selectedPersonToCreate).length === peopleToCreate.length
|
||||
) {
|
||||
clearTimeout(loaderLoadingDoneTimeout);
|
||||
clearTimeout(automaticRefreshTimeout);
|
||||
@@ -97,36 +97,41 @@
|
||||
dispatch('close');
|
||||
};
|
||||
|
||||
const handleReset = (index: number) => {
|
||||
if (selectedPersonToReassign[index]) {
|
||||
selectedPersonToReassign[index] = null;
|
||||
const handleReset = (id: string) => {
|
||||
if (selectedPersonToReassign[id]) {
|
||||
delete selectedPersonToReassign[id];
|
||||
|
||||
// trigger reactivity
|
||||
selectedPersonToReassign = selectedPersonToReassign;
|
||||
}
|
||||
if (selectedPersonToCreate[index]) {
|
||||
selectedPersonToCreate[index] = null;
|
||||
if (selectedPersonToCreate[id]) {
|
||||
delete selectedPersonToCreate[id];
|
||||
|
||||
// trigger reactivity
|
||||
selectedPersonToCreate = selectedPersonToCreate;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditFaces = async () => {
|
||||
loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), timeBeforeShowLoadingSpinner);
|
||||
const numberOfChanges =
|
||||
selectedPersonToCreate.filter((person) => person !== null).length +
|
||||
selectedPersonToReassign.filter((person) => person !== null).length;
|
||||
const numberOfChanges = Object.keys(selectedPersonToCreate).length + Object.keys(selectedPersonToReassign).length;
|
||||
|
||||
if (numberOfChanges > 0) {
|
||||
try {
|
||||
for (const [index, peopleWithFace] of peopleWithFaces.entries()) {
|
||||
const personId = selectedPersonToReassign[index]?.id;
|
||||
for (const personWithFace of peopleWithFaces) {
|
||||
const personId = selectedPersonToReassign[personWithFace.id]?.id;
|
||||
|
||||
if (personId) {
|
||||
await reassignFacesById({
|
||||
id: personId,
|
||||
faceDto: { id: peopleWithFace.id },
|
||||
faceDto: { id: personWithFace.id },
|
||||
});
|
||||
} else if (selectedPersonToCreate[index]) {
|
||||
} else if (selectedPersonToCreate[personWithFace.id]) {
|
||||
const data = await createPerson({ personCreateDto: {} });
|
||||
numberOfPersonToCreate.push(data.id);
|
||||
peopleToCreate.push(data.id);
|
||||
await reassignFacesById({
|
||||
id: data.id,
|
||||
faceDto: { id: peopleWithFace.id },
|
||||
faceDto: { id: personWithFace.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -141,7 +146,7 @@
|
||||
}
|
||||
|
||||
isShowLoadingDone = false;
|
||||
if (numberOfPersonToCreate.length === 0) {
|
||||
if (peopleToCreate.length === 0) {
|
||||
clearTimeout(loaderLoadingDoneTimeout);
|
||||
dispatch('refresh');
|
||||
} else {
|
||||
@@ -150,23 +155,26 @@
|
||||
};
|
||||
|
||||
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) {
|
||||
selectedPersonToCreate[peopleWithFaces.indexOf(personToUpdate)] = newFeaturePhoto;
|
||||
selectedPersonToCreate[personToUpdate.id] = newFeaturePhoto;
|
||||
}
|
||||
showSeletecFaces = false;
|
||||
};
|
||||
|
||||
const handleReassignFace = (person: PersonResponseDto | null) => {
|
||||
if (person) {
|
||||
selectedPersonToReassign[editedPersonIndex] = person;
|
||||
const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
|
||||
if (person && personToUpdate) {
|
||||
selectedPersonToReassign[personToUpdate.id] = person;
|
||||
showSeletecFaces = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePersonPicker = (index: number) => {
|
||||
editedPersonIndex = index;
|
||||
showSeletecFaces = true;
|
||||
const handlePersonPicker = (person: PersonResponseDto | null) => {
|
||||
if (person) {
|
||||
editedPerson = person;
|
||||
showSeletecFaces = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -217,35 +225,48 @@
|
||||
on:mouseleave={() => ($boundingBoxesArray = [])}
|
||||
>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={selectedPersonToCreate[index] ||
|
||||
getPeopleThumbnailUrl(selectedPersonToReassign[index]?.id || face.person.id)}
|
||||
altText={selectedPersonToReassign[index]
|
||||
? selectedPersonToReassign[index]?.name
|
||||
: selectedPersonToCreate[index]
|
||||
? 'New person'
|
||||
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)}
|
||||
title={selectedPersonToReassign[index]
|
||||
? selectedPersonToReassign[index]?.name
|
||||
: selectedPersonToCreate[index]
|
||||
? 'New person'
|
||||
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
thumbhash={null}
|
||||
hidden={selectedPersonToReassign[index]
|
||||
? selectedPersonToReassign[index]?.isHidden
|
||||
: selectedPersonToCreate[index]
|
||||
? false
|
||||
: face.person?.isHidden}
|
||||
/>
|
||||
{#if selectedPersonToCreate[face.id]}
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={selectedPersonToCreate[face.id]}
|
||||
altText={selectedPersonToCreate[face.id]}
|
||||
title={'New person'}
|
||||
widthStyle={thumbnailWidth}
|
||||
heightStyle={thumbnailWidth}
|
||||
/>
|
||||
{:else if selectedPersonToReassign[face.id]}
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(selectedPersonToReassign[face.id].id)}
|
||||
altText={selectedPersonToReassign[face.id]?.name || selectedPersonToReassign[face.id].id}
|
||||
title={getPersonNameWithHiddenValue(
|
||||
selectedPersonToReassign[face.id].name,
|
||||
face.person?.isHidden,
|
||||
)}
|
||||
widthStyle={thumbnailWidth}
|
||||
heightStyle={thumbnailWidth}
|
||||
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>
|
||||
{#if !selectedPersonToCreate[index]}
|
||||
|
||||
{#if !selectedPersonToCreate[face.id]}
|
||||
<p class="relative mt-1 truncate font-medium" title={face.person?.name}>
|
||||
{#if selectedPersonToReassign[index]?.id}
|
||||
{selectedPersonToReassign[index]?.name}
|
||||
{#if selectedPersonToReassign[face.id]?.id}
|
||||
{selectedPersonToReassign[face.id]?.name}
|
||||
{:else}
|
||||
{face.person?.name}
|
||||
{/if}
|
||||
@@ -253,8 +274,8 @@
|
||||
{/if}
|
||||
|
||||
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700">
|
||||
{#if selectedPersonToCreate[index] || selectedPersonToReassign[index]}
|
||||
<button on:click={() => handleReset(index)} class="flex h-full w-full">
|
||||
{#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
|
||||
<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>
|
||||
<Icon path={mdiRestart} size={18} />
|
||||
@@ -262,7 +283,7 @@
|
||||
</div>
|
||||
</button>
|
||||
{: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
|
||||
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
|
||||
{peopleWithFaces}
|
||||
{allPeople}
|
||||
{editedPersonIndex}
|
||||
{editedPerson}
|
||||
{assetType}
|
||||
{assetId}
|
||||
on:close={() => (showSeletecFaces = false)}
|
||||
|
||||
@@ -1,20 +1,45 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import type { OnStack } from '$lib/utils/actions';
|
||||
import { stackAssets } from '$lib/utils/asset-utils';
|
||||
import { mdiImageMultipleOutline } from '@mdi/js';
|
||||
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import { mdiImageMinusOutline, mdiImageMultipleOutline } from '@mdi/js';
|
||||
import { stackAssets, unstackAssets } from '$lib/utils/asset-utils';
|
||||
import type { OnStack, OnUnstack } from '$lib/utils/actions';
|
||||
|
||||
export let unstack = false;
|
||||
export let onStack: OnStack | undefined;
|
||||
export let onUnstack: OnUnstack | undefined;
|
||||
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
const handleStack = async () => {
|
||||
await stackAssets([...getOwnedAssets()], (ids) => {
|
||||
const selectedAssets = [...getOwnedAssets()];
|
||||
const ids = await stackAssets(selectedAssets);
|
||||
if (ids) {
|
||||
onStack?.(ids);
|
||||
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>
|
||||
|
||||
<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 () => {
|
||||
if ($selectedAssets.size > 1) {
|
||||
await stackAssets(Array.from($selectedAssets), (ids) => {
|
||||
assetStore.removeAssets(ids);
|
||||
dispatch('escape');
|
||||
});
|
||||
const ids = await stackAssets(Array.from($selectedAssets));
|
||||
if (ids) {
|
||||
assetStore.removeAssets(ids);
|
||||
dispatch('escape');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,6 +106,8 @@
|
||||
{ shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
|
||||
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
|
||||
{ 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) {
|
||||
|
||||
@@ -4,27 +4,34 @@
|
||||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiShuffle } from '@mdi/js';
|
||||
import { SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiFitToPageOutline, mdiFitToScreenOutline, mdiShuffle } from '@mdi/js';
|
||||
import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
|
||||
import Button from './elements/buttons/button.svelte';
|
||||
import type { RenderedOption } from './elements/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 = () => {};
|
||||
|
||||
const options: Record<SlideshowNavigation, RenderedOption> = {
|
||||
const navigationOptions: Record<SlideshowNavigation, RenderedOption> = {
|
||||
[SlideshowNavigation.Shuffle]: { icon: mdiShuffle, title: 'Shuffle' },
|
||||
[SlideshowNavigation.AscendingOrder]: { icon: mdiArrowUpThin, title: 'Backward' },
|
||||
[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)) {
|
||||
if (option === selectedOption) {
|
||||
$slideshowNavigation = key as SlideshowNavigation;
|
||||
break;
|
||||
if (option === record) {
|
||||
return key as Type;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -34,9 +41,19 @@
|
||||
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<SettingDropdown
|
||||
title="Direction"
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[$slideshowNavigation]}
|
||||
onToggle={(option) => handleToggle(option)}
|
||||
options={Object.values(navigationOptions)}
|
||||
selectedOption={navigationOptions[$slideshowNavigation]}
|
||||
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} />
|
||||
<SettingInputField
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { AuthDeviceResponseDto } from '@immich/sdk';
|
||||
import type { SessionResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
mdiAndroid,
|
||||
mdiApple,
|
||||
@@ -15,7 +15,7 @@
|
||||
import { DateTime, type ToRelativeCalendarOptions } from 'luxon';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let device: AuthDeviceResponseDto;
|
||||
export let device: SessionResponseDto;
|
||||
|
||||
const dispatcher = createEventDispatcher<{
|
||||
delete: void;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<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 Button from '../elements/buttons/button.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';
|
||||
|
||||
export let devices: AuthDeviceResponseDto[];
|
||||
let deleteDevice: AuthDeviceResponseDto | null = null;
|
||||
export let devices: SessionResponseDto[];
|
||||
let deleteDevice: SessionResponseDto | null = null;
|
||||
let deleteAll = false;
|
||||
|
||||
const refresh = () => getAuthDevices().then((_devices) => (devices = _devices));
|
||||
const refresh = () => getSessions().then((_devices) => (devices = _devices));
|
||||
|
||||
$: currentDevice = devices.find((device) => device.current);
|
||||
$: otherDevices = devices.filter((device) => !device.current);
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
await logoutAuthDevice({ id: deleteDevice.id });
|
||||
await deleteSession({ id: deleteDevice.id });
|
||||
notificationController.show({ message: `Logged out device`, type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to log out device');
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
const handleDeleteAll = async () => {
|
||||
try {
|
||||
await logoutAuthDevices();
|
||||
await deleteAllSessions();
|
||||
notificationController.show({
|
||||
message: `Logged out all devices`,
|
||||
type: NotificationType.Info,
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
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 AppSettings from './app-settings.svelte';
|
||||
import ChangePasswordSettings from './change-password-settings.svelte';
|
||||
@@ -14,10 +15,9 @@
|
||||
import PartnerSettings from './partner-settings.svelte';
|
||||
import UserAPIKeyList from './user-api-key-list.svelte';
|
||||
import UserProfileSettings from './user-profile-settings.svelte';
|
||||
import SettingAccordionState from '../shared-components/settings/setting-accordion-state.svelte';
|
||||
|
||||
export let keys: ApiKeyResponseDto[] = [];
|
||||
export let devices: AuthDeviceResponseDto[] = [];
|
||||
export let sessions: SessionResponseDto[] = [];
|
||||
|
||||
let oauthOpen =
|
||||
oauth.isCallback(window.location) ||
|
||||
@@ -38,7 +38,7 @@
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion key="authorized-devices" title="Authorized Devices" subtitle="Manage your logged-in devices">
|
||||
<DeviceList bind:devices />
|
||||
<DeviceList bind:devices={sessions} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion key="memories" title="Memories" subtitle="Manage what you see in your memories">
|
||||
|
||||
@@ -13,6 +13,16 @@ export enum SlideshowNavigation {
|
||||
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() {
|
||||
const restartState = writable<boolean>(false);
|
||||
const stopState = writable<boolean>(false);
|
||||
@@ -21,6 +31,7 @@ function createSlideshowStore() {
|
||||
'slideshow-navigation',
|
||||
SlideshowNavigation.DescendingOrder,
|
||||
);
|
||||
const slideshowLook = persisted<SlideshowLook>('slideshow-look', SlideshowLook.Contain);
|
||||
const slideshowState = writable<SlideshowState>(SlideshowState.None);
|
||||
|
||||
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
|
||||
@@ -50,6 +61,7 @@ function createSlideshowStore() {
|
||||
},
|
||||
},
|
||||
slideshowNavigation,
|
||||
slideshowLook,
|
||||
slideshowState,
|
||||
slideshowDelay,
|
||||
showProgressBar,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
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 OnFavorite = (ids: string[], favorite: boolean) => void;
|
||||
export type OnStack = (ids: string[]) => void;
|
||||
export type OnUnstack = (assets: AssetResponseDto[]) => void;
|
||||
|
||||
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { goto } from '$app/navigation';
|
||||
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
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 { downloadManager } from '$lib/stores/download';
|
||||
import { downloadRequest, getKey } from '$lib/utils';
|
||||
@@ -269,43 +270,81 @@ export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserRespo
|
||||
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 {
|
||||
const parent = assets.at(0);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
await updateAssets({
|
||||
assetBulkUpdateDto: {
|
||||
ids,
|
||||
stackParentId: parent.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to stack assets');
|
||||
return false;
|
||||
}
|
||||
|
||||
const children = assets.slice(1);
|
||||
const ids = children.map(({ id }) => id);
|
||||
|
||||
if (children.length > 0) {
|
||||
await updateAssets({ assetBulkUpdateDto: { ids, stackParentId: parent.id } });
|
||||
}
|
||||
|
||||
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;
|
||||
let grandChildren: AssetResponseDto[] = [];
|
||||
for (const asset of children) {
|
||||
asset.stackParentId = parent.id;
|
||||
if (asset.stack) {
|
||||
// Add grand-children to new parent
|
||||
grandChildren = grandChildren.concat(asset.stack);
|
||||
// Reset children stack info
|
||||
asset.stackCount = null;
|
||||
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) => {
|
||||
if (get(isSelectingAllAssets)) {
|
||||
|
||||
@@ -30,7 +30,14 @@
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
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 = () => {
|
||||
if ($showAssetViewer) {
|
||||
@@ -62,8 +69,12 @@
|
||||
<FavoriteAction removeFavorite={isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
||||
<AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
|
||||
<DownloadAction menuItem />
|
||||
{#if $selectedAssets.size > 1}
|
||||
<StackAction onStack={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||
{#if $selectedAssets.size > 1 || isAssetStackSelected}
|
||||
<StackAction
|
||||
unstack={isAssetStackSelected}
|
||||
onStack={(assetIds) => assetStore.removeAssets(assetIds)}
|
||||
onUnstack={(assets) => assetStore.addAssets(assets)}
|
||||
/>
|
||||
{/if}
|
||||
<ChangeDate menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</svelte:fragment>
|
||||
<section class="mx-4 flex place-content-center">
|
||||
<div class="w-full max-w-3xl">
|
||||
<UserSettingsList keys={data.keys} devices={data.devices} />
|
||||
<UserSettingsList keys={data.keys} sessions={data.sessions} />
|
||||
</div>
|
||||
</section>
|
||||
</UserPageLayout>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getApiKeys, getAuthDevices } from '@immich/sdk';
|
||||
import { getApiKeys, getSessions } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
await authenticate();
|
||||
|
||||
const keys = await getApiKeys();
|
||||
const devices = await getAuthDevices();
|
||||
const sessions = await getSessions();
|
||||
|
||||
return {
|
||||
keys,
|
||||
devices,
|
||||
sessions,
|
||||
meta: {
|
||||
title: 'Settings',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user