Compare commits
1 Commits
v1.102.0
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fd5a32a7d |
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine3.19@sha256:ec0c413b1d84f3f7f67ec986ba885930c57b5318d2eb3abc6960ee05d4f2eb28 as core
|
FROM node:20-alpine3.19@sha256:7e227295e96f5b00aa79555ae166f50610940d888fc2e321cf36304cbd17d7d6 as core
|
||||||
|
|
||||||
WORKDIR /usr/src/open-api/typescript-sdk
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
|
|||||||
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@@ -47,15 +47,15 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.12.7",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.4.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
|||||||
|
|
||||||
Hostname
|
Hostname
|
||||||
|
|
||||||
- `https://immich.example.com/auth/login`
|
- `https://immich.example.com/auth/login`)
|
||||||
- `https://immich.example.com/user-settings`
|
- `https://immich.example.com/user-settings`)
|
||||||
|
|
||||||
## Enable OAuth
|
## Enable OAuth
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
|
|||||||
CREATE EXTENSION vectors;
|
CREATE EXTENSION vectors;
|
||||||
CREATE EXTENSION earthdistance CASCADE;
|
CREATE EXTENSION earthdistance CASCADE;
|
||||||
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
||||||
ALTER SCHEMA vectors OWNER TO <immichdbusername>;
|
GRANT USAGE ON SCHEMA vectors TO <immichdbusername>;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO <immichdbusername>;
|
||||||
COMMIT;
|
COMMIT;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -120,8 +120,7 @@ The default configuration looks like this:
|
|||||||
"previewFormat": "jpeg",
|
"previewFormat": "jpeg",
|
||||||
"previewSize": 1440,
|
"previewSize": 1440,
|
||||||
"quality": 80,
|
"quality": 80,
|
||||||
"colorspace": "p3",
|
"colorspace": "p3"
|
||||||
"extractEmbedded": false
|
|
||||||
},
|
},
|
||||||
"newVersionCheck": {
|
"newVersionCheck": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
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).
|
Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level.
|
||||||
|
|
||||||
:::note new version
|
:::note new version
|
||||||
On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further).
|
On new machines running version 1.92.0 storage template engine is off by default, for [more info](https://github.com/immich-app/immich/releases/tag/v1.92.0#:~:text=the%20partner%E2%80%99s%20assets.-,Hardening%20storage%20template,-We%20have%20further).
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
<<: *server-common
|
<<: *server-common
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672
|
image: redis:6.2-alpine@sha256:3fcb624d83a9c478357f16dc173c58ded325ccc5fd2a4375f3916c04cc579f70
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||||
|
|||||||
10
e2e/package-lock.json
generated
10
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
@@ -81,15 +81,15 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.12.7",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.4.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LoginResponseDto, login, signUpAdmin } from '@immich/sdk';
|
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
|
||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { app, asBearerAuth, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -112,29 +112,70 @@ describe('/auth/*', () => {
|
|||||||
|
|
||||||
const cookies = headers['set-cookie'];
|
const cookies = headers['set-cookie'];
|
||||||
expect(cookies).toHaveLength(3);
|
expect(cookies).toHaveLength(3);
|
||||||
expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
|
expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
|
||||||
`immich_access_token=${token}`,
|
expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;');
|
||||||
'Max-Age=34560000',
|
expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;');
|
||||||
'Path=/',
|
});
|
||||||
expect.stringContaining('Expires='),
|
});
|
||||||
'HttpOnly',
|
|
||||||
'SameSite=Lax',
|
describe('GET /auth/devices', () => {
|
||||||
]);
|
it('should require authentication', async () => {
|
||||||
expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
|
const { status, body } = await request(app).get('/auth/devices');
|
||||||
'immich_auth_type=password',
|
expect(status).toBe(401);
|
||||||
'Max-Age=34560000',
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
'Path=/',
|
});
|
||||||
expect.stringContaining('Expires='),
|
|
||||||
'HttpOnly',
|
it('should get a list of authorized devices', async () => {
|
||||||
'SameSite=Lax',
|
const { status, body } = await request(app)
|
||||||
]);
|
.get('/auth/devices')
|
||||||
expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
'immich_is_authenticated=true',
|
expect(status).toBe(200);
|
||||||
'Max-Age=34560000',
|
expect(body).toEqual([deviceDto.current]);
|
||||||
'Path=/',
|
});
|
||||||
expect.stringContaining('Expires='),
|
});
|
||||||
'SameSite=Lax',
|
|
||||||
]);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
|
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
@@ -32,9 +32,6 @@ describe('/search', () => {
|
|||||||
let assetGlarus: AssetFileUploadResponseDto;
|
let assetGlarus: AssetFileUploadResponseDto;
|
||||||
let assetSprings: AssetFileUploadResponseDto;
|
let assetSprings: AssetFileUploadResponseDto;
|
||||||
let assetLast: AssetFileUploadResponseDto;
|
let assetLast: AssetFileUploadResponseDto;
|
||||||
let cities: string[];
|
|
||||||
let states: string[];
|
|
||||||
let countries: string[];
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
@@ -82,7 +79,7 @@ describe('/search', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
||||||
const coordinates = [
|
const cities = [
|
||||||
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
||||||
{ latitude: 63.0695, longitude: -151.0074 }, // denali
|
{ latitude: 63.0695, longitude: -151.0074 }, // denali
|
||||||
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
||||||
@@ -104,7 +101,7 @@ describe('/search', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const updates = assets.map((asset, i) =>
|
const updates = assets.map((asset, i) =>
|
||||||
updateAsset({ id: asset.id, updateAssetDto: coordinates[i] }, { headers: asBearerAuth(admin.accessToken) }),
|
updateAsset({ id: asset.id, updateAssetDto: cities[i] }, { headers: asBearerAuth(admin.accessToken) }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(updates);
|
await Promise.all(updates);
|
||||||
@@ -136,12 +133,6 @@ describe('/search', () => {
|
|||||||
assetLast = assets.at(-1) as AssetFileUploadResponseDto;
|
assetLast = assets.at(-1) as AssetFileUploadResponseDto;
|
||||||
|
|
||||||
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
|
||||||
const mapMarkers = await getMapMarkers({}, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
const nonTrashed = mapMarkers.filter((mark) => mark.id !== assetSilver.id);
|
|
||||||
cities = [...new Set(nonTrashed.map((mark) => mark.city).filter((entry): entry is string => !!entry))].sort();
|
|
||||||
states = [...new Set(nonTrashed.map((mark) => mark.state).filter((entry): entry is string => !!entry))].sort();
|
|
||||||
countries = [...new Set(nonTrashed.map((mark) => mark.country).filter((entry): entry is string => !!entry))].sort();
|
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -461,7 +452,21 @@ describe('/search', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/search/suggestions?type=country')
|
.get('/search/suggestions?type=country')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(body).toEqual(countries);
|
expect(body).toEqual([
|
||||||
|
'Cuba',
|
||||||
|
'France',
|
||||||
|
'Georgia',
|
||||||
|
'Germany',
|
||||||
|
'Ghana',
|
||||||
|
'Japan',
|
||||||
|
'Morocco',
|
||||||
|
"People's Republic of China",
|
||||||
|
'Russian Federation',
|
||||||
|
'Singapore',
|
||||||
|
'Spain',
|
||||||
|
'Switzerland',
|
||||||
|
'United States of America',
|
||||||
|
]);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -469,7 +474,23 @@ describe('/search', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/search/suggestions?type=state')
|
.get('/search/suggestions?type=state')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(body).toEqual(states);
|
expect(body).toEqual([
|
||||||
|
'Accra, Greater Accra',
|
||||||
|
'Berlin',
|
||||||
|
'Glarus, Glarus',
|
||||||
|
'Havana',
|
||||||
|
'Marrakech, Marrakesh-Safi',
|
||||||
|
'Mesa County, Colorado',
|
||||||
|
'Neshoba County, Mississippi',
|
||||||
|
'New York',
|
||||||
|
'Page County, Virginia',
|
||||||
|
'Paris, Île-de-France',
|
||||||
|
'Province of Córdoba, Andalusia',
|
||||||
|
'Shanghai Municipality, Shanghai',
|
||||||
|
'St.-Petersburg',
|
||||||
|
'Tbilisi',
|
||||||
|
'Tokyo',
|
||||||
|
]);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -477,7 +498,24 @@ describe('/search', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/search/suggestions?type=city')
|
.get('/search/suggestions?type=city')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(body).toEqual(cities);
|
expect(body).toEqual([
|
||||||
|
'Accra',
|
||||||
|
'Berlin',
|
||||||
|
'Glarus',
|
||||||
|
'Havana',
|
||||||
|
'Marrakesh',
|
||||||
|
'Montalbán de Córdoba',
|
||||||
|
'New York City',
|
||||||
|
'Palisade',
|
||||||
|
'Paris',
|
||||||
|
'Philadelphia',
|
||||||
|
'Saint Petersburg',
|
||||||
|
'Shanghai',
|
||||||
|
'Singapore',
|
||||||
|
'Stanley',
|
||||||
|
'Tbilisi',
|
||||||
|
'Tokyo',
|
||||||
|
]);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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'");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -43,7 +43,7 @@ import { loginDto, signupDto } from 'src/fixtures';
|
|||||||
import { makeRandomImage } from 'src/generators';
|
import { makeRandomImage } from 'src/generators';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
type CliResponse = { stdout: string; stderr: string; exitCode: number | null };
|
||||||
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete';
|
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete';
|
||||||
type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number };
|
type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number };
|
||||||
type AdminSetupOptions = { onboarding?: boolean };
|
type AdminSetupOptions = { onboarding?: boolean };
|
||||||
@@ -59,15 +59,13 @@ export const testAssetDirInternal = '/data/assets';
|
|||||||
export const tempDir = tmpdir();
|
export const tempDir = tmpdir();
|
||||||
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
|
||||||
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
||||||
export const immichCli = (args: string[]) =>
|
export const immichCli = async (args: string[]) => {
|
||||||
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]);
|
let _resolve: (value: CliResponse) => void;
|
||||||
export const immichAdmin = (args: string[]) =>
|
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
|
||||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args];
|
||||||
|
const child = spawn('node', _args, {
|
||||||
const executeCommand = (command: string, args: string[]) => {
|
stdio: 'pipe',
|
||||||
let _resolve: (value: CommandResponse) => void;
|
});
|
||||||
const deferred = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
|
||||||
const child = spawn(command, args, { stdio: 'pipe' });
|
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
@@ -140,7 +138,7 @@ export const utils = {
|
|||||||
'asset_faces',
|
'asset_faces',
|
||||||
'activity',
|
'activity',
|
||||||
'api_keys',
|
'api_keys',
|
||||||
'sessions',
|
'user_token',
|
||||||
'users',
|
'users',
|
||||||
'system_metadata',
|
'system_metadata',
|
||||||
'system_config',
|
'system_config',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ try {
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'],
|
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
||||||
globalSetup,
|
globalSetup,
|
||||||
testTimeout: 15_000,
|
testTimeout: 15_000,
|
||||||
poolOptions: {
|
poolOptions: {
|
||||||
|
|||||||
11
machine-learning/poetry.lock
generated
11
machine-learning/poetry.lock
generated
@@ -1150,23 +1150,22 @@ test = ["objgraph", "psutil"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gunicorn"
|
name = "gunicorn"
|
||||||
version = "22.0.0"
|
version = "21.2.0"
|
||||||
description = "WSGI HTTP Server for UNIX"
|
description = "WSGI HTTP Server for UNIX"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.5"
|
||||||
files = [
|
files = [
|
||||||
{file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"},
|
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"},
|
||||||
{file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"},
|
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
eventlet = ["eventlet (>=0.24.1,!=0.36.0)"]
|
eventlet = ["eventlet (>=0.24.1)"]
|
||||||
gevent = ["gevent (>=1.4.0)"]
|
gevent = ["gevent (>=1.4.0)"]
|
||||||
setproctitle = ["setproctitle"]
|
setproctitle = ["setproctitle"]
|
||||||
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
|
|
||||||
tornado = ["tornado (>=0.2)"]
|
tornado = ["tornado (>=0.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.102.0"
|
version = "1.101.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 132,
|
"android.injected.version.code" => 131,
|
||||||
"android.injected.version.name" => "1.102.0",
|
"android.injected.version.name" => "1.101.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
BIN
mobile/fonts/SnowburstOne.ttf
Normal file
BIN
mobile/fonts/SnowburstOne.ttf
Normal file
Binary file not shown.
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.102.0"
|
version_number: "1.101.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -39,24 +39,27 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
buildTopRow() {
|
buildTopRow() {
|
||||||
return Stack(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
InkWell(
|
||||||
alignment: Alignment.topLeft,
|
onTap: () => context.pop(),
|
||||||
child: InkWell(
|
child: const Icon(
|
||||||
onTap: () => context.pop(),
|
Icons.close,
|
||||||
child: const Icon(
|
size: 20,
|
||||||
Icons.close,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Center(
|
Expanded(
|
||||||
child: Image.asset(
|
child: Align(
|
||||||
context.isDarkTheme
|
alignment: Alignment.center,
|
||||||
? 'assets/immich-text-dark.png'
|
child: Text(
|
||||||
: 'assets/immich-text-light.png',
|
'IMMICH',
|
||||||
height: 16,
|
style: TextStyle(
|
||||||
|
fontFamily: 'SnowburstOne',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: context.primaryColor,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
9
mobile/openapi/.openapi-generator/FILES
generated
9
mobile/openapi/.openapi-generator/FILES
generated
@@ -41,6 +41,7 @@ doc/AssetTypeEnum.md
|
|||||||
doc/AudioCodec.md
|
doc/AudioCodec.md
|
||||||
doc/AuditApi.md
|
doc/AuditApi.md
|
||||||
doc/AuditDeletesResponseDto.md
|
doc/AuditDeletesResponseDto.md
|
||||||
|
doc/AuthDeviceResponseDto.md
|
||||||
doc/AuthenticationApi.md
|
doc/AuthenticationApi.md
|
||||||
doc/BulkIdResponseDto.md
|
doc/BulkIdResponseDto.md
|
||||||
doc/BulkIdsDto.md
|
doc/BulkIdsDto.md
|
||||||
@@ -141,8 +142,6 @@ doc/ServerPingResponse.md
|
|||||||
doc/ServerStatsResponseDto.md
|
doc/ServerStatsResponseDto.md
|
||||||
doc/ServerThemeDto.md
|
doc/ServerThemeDto.md
|
||||||
doc/ServerVersionResponseDto.md
|
doc/ServerVersionResponseDto.md
|
||||||
doc/SessionResponseDto.md
|
|
||||||
doc/SessionsApi.md
|
|
||||||
doc/SharedLinkApi.md
|
doc/SharedLinkApi.md
|
||||||
doc/SharedLinkCreateDto.md
|
doc/SharedLinkCreateDto.md
|
||||||
doc/SharedLinkEditDto.md
|
doc/SharedLinkEditDto.md
|
||||||
@@ -220,7 +219,6 @@ lib/api/partner_api.dart
|
|||||||
lib/api/person_api.dart
|
lib/api/person_api.dart
|
||||||
lib/api/search_api.dart
|
lib/api/search_api.dart
|
||||||
lib/api/server_info_api.dart
|
lib/api/server_info_api.dart
|
||||||
lib/api/sessions_api.dart
|
|
||||||
lib/api/shared_link_api.dart
|
lib/api/shared_link_api.dart
|
||||||
lib/api/sync_api.dart
|
lib/api/sync_api.dart
|
||||||
lib/api/system_config_api.dart
|
lib/api/system_config_api.dart
|
||||||
@@ -269,6 +267,7 @@ lib/model/asset_stats_response_dto.dart
|
|||||||
lib/model/asset_type_enum.dart
|
lib/model/asset_type_enum.dart
|
||||||
lib/model/audio_codec.dart
|
lib/model/audio_codec.dart
|
||||||
lib/model/audit_deletes_response_dto.dart
|
lib/model/audit_deletes_response_dto.dart
|
||||||
|
lib/model/auth_device_response_dto.dart
|
||||||
lib/model/bulk_id_response_dto.dart
|
lib/model/bulk_id_response_dto.dart
|
||||||
lib/model/bulk_ids_dto.dart
|
lib/model/bulk_ids_dto.dart
|
||||||
lib/model/change_password_dto.dart
|
lib/model/change_password_dto.dart
|
||||||
@@ -358,7 +357,6 @@ lib/model/server_ping_response.dart
|
|||||||
lib/model/server_stats_response_dto.dart
|
lib/model/server_stats_response_dto.dart
|
||||||
lib/model/server_theme_dto.dart
|
lib/model/server_theme_dto.dart
|
||||||
lib/model/server_version_response_dto.dart
|
lib/model/server_version_response_dto.dart
|
||||||
lib/model/session_response_dto.dart
|
|
||||||
lib/model/shared_link_create_dto.dart
|
lib/model/shared_link_create_dto.dart
|
||||||
lib/model/shared_link_edit_dto.dart
|
lib/model/shared_link_edit_dto.dart
|
||||||
lib/model/shared_link_response_dto.dart
|
lib/model/shared_link_response_dto.dart
|
||||||
@@ -450,6 +448,7 @@ test/asset_type_enum_test.dart
|
|||||||
test/audio_codec_test.dart
|
test/audio_codec_test.dart
|
||||||
test/audit_api_test.dart
|
test/audit_api_test.dart
|
||||||
test/audit_deletes_response_dto_test.dart
|
test/audit_deletes_response_dto_test.dart
|
||||||
|
test/auth_device_response_dto_test.dart
|
||||||
test/authentication_api_test.dart
|
test/authentication_api_test.dart
|
||||||
test/bulk_id_response_dto_test.dart
|
test/bulk_id_response_dto_test.dart
|
||||||
test/bulk_ids_dto_test.dart
|
test/bulk_ids_dto_test.dart
|
||||||
@@ -550,8 +549,6 @@ test/server_ping_response_test.dart
|
|||||||
test/server_stats_response_dto_test.dart
|
test/server_stats_response_dto_test.dart
|
||||||
test/server_theme_dto_test.dart
|
test/server_theme_dto_test.dart
|
||||||
test/server_version_response_dto_test.dart
|
test/server_version_response_dto_test.dart
|
||||||
test/session_response_dto_test.dart
|
|
||||||
test/sessions_api_test.dart
|
|
||||||
test/shared_link_api_test.dart
|
test/shared_link_api_test.dart
|
||||||
test/shared_link_create_dto_test.dart
|
test/shared_link_create_dto_test.dart
|
||||||
test/shared_link_edit_dto_test.dart
|
test/shared_link_edit_dto_test.dart
|
||||||
|
|||||||
10
mobile/openapi/README.md
generated
10
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.102.0
|
- API version: 1.101.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -117,8 +117,11 @@ Class | Method | HTTP request | Description
|
|||||||
*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report |
|
*AuditApi* | [**getAuditFiles**](doc//AuditApi.md#getauditfiles) | **GET** /audit/file-report |
|
||||||
*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
|
*AuditApi* | [**getFileChecksums**](doc//AuditApi.md#getfilechecksums) | **POST** /audit/file-report/checksum |
|
||||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||||
|
*AuthenticationApi* | [**getAuthDevices**](doc//AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
|
||||||
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
||||||
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
||||||
|
*AuthenticationApi* | [**logoutAuthDevice**](doc//AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} |
|
||||||
|
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
|
||||||
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
||||||
@@ -180,9 +183,6 @@ Class | Method | HTTP request | Description
|
|||||||
*ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
|
*ServerInfoApi* | [**getTheme**](doc//ServerInfoApi.md#gettheme) | **GET** /server-info/theme |
|
||||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||||
*ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding |
|
*ServerInfoApi* | [**setAdminOnboarding**](doc//ServerInfoApi.md#setadminonboarding) | **POST** /server-info/admin-onboarding |
|
||||||
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
|
|
||||||
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
|
|
||||||
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
|
|
||||||
*SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets |
|
*SharedLinkApi* | [**addSharedLinkAssets**](doc//SharedLinkApi.md#addsharedlinkassets) | **PUT** /shared-link/{id}/assets |
|
||||||
*SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
|
*SharedLinkApi* | [**createSharedLink**](doc//SharedLinkApi.md#createsharedlink) | **POST** /shared-link |
|
||||||
*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
|
*SharedLinkApi* | [**getAllSharedLinks**](doc//SharedLinkApi.md#getallsharedlinks) | **GET** /shared-link |
|
||||||
@@ -258,6 +258,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||||
- [AudioCodec](doc//AudioCodec.md)
|
- [AudioCodec](doc//AudioCodec.md)
|
||||||
- [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md)
|
- [AuditDeletesResponseDto](doc//AuditDeletesResponseDto.md)
|
||||||
|
- [AuthDeviceResponseDto](doc//AuthDeviceResponseDto.md)
|
||||||
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
||||||
- [BulkIdsDto](doc//BulkIdsDto.md)
|
- [BulkIdsDto](doc//BulkIdsDto.md)
|
||||||
- [CLIPConfig](doc//CLIPConfig.md)
|
- [CLIPConfig](doc//CLIPConfig.md)
|
||||||
@@ -347,7 +348,6 @@ Class | Method | HTTP request | Description
|
|||||||
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
||||||
- [ServerThemeDto](doc//ServerThemeDto.md)
|
- [ServerThemeDto](doc//ServerThemeDto.md)
|
||||||
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
||||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
|
||||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# openapi.model.SessionResponseDto
|
# openapi.model.AuthDeviceResponseDto
|
||||||
|
|
||||||
## Load the model package
|
## Load the model package
|
||||||
```dart
|
```dart
|
||||||
158
mobile/openapi/doc/AuthenticationApi.md
generated
158
mobile/openapi/doc/AuthenticationApi.md
generated
@@ -10,8 +10,11 @@ All URIs are relative to */api*
|
|||||||
Method | HTTP request | Description
|
Method | HTTP request | Description
|
||||||
------------- | ------------- | -------------
|
------------- | ------------- | -------------
|
||||||
[**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
[**changePassword**](AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||||
|
[**getAuthDevices**](AuthenticationApi.md#getauthdevices) | **GET** /auth/devices |
|
||||||
[**login**](AuthenticationApi.md#login) | **POST** /auth/login |
|
[**login**](AuthenticationApi.md#login) | **POST** /auth/login |
|
||||||
[**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout |
|
[**logout**](AuthenticationApi.md#logout) | **POST** /auth/logout |
|
||||||
|
[**logoutAuthDevice**](AuthenticationApi.md#logoutauthdevice) | **DELETE** /auth/devices/{id} |
|
||||||
|
[**logoutAuthDevices**](AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
|
||||||
[**signUpAdmin**](AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
[**signUpAdmin**](AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||||
[**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
[**validateAccessToken**](AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||||
|
|
||||||
@@ -71,6 +74,57 @@ Name | Type | Description | Notes
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **getAuthDevices**
|
||||||
|
> List<AuthDeviceResponseDto> getAuthDevices()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure API key authorization: cookie
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = AuthenticationApi();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.getAuthDevices();
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AuthenticationApi->getAuthDevices: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
This endpoint does not need any parameter.
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**List<AuthDeviceResponseDto>**](AuthDeviceResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **login**
|
# **login**
|
||||||
> LoginResponseDto login(loginCredentialDto)
|
> LoginResponseDto login(loginCredentialDto)
|
||||||
|
|
||||||
@@ -163,6 +217,110 @@ This endpoint does not need any parameter.
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **logoutAuthDevice**
|
||||||
|
> logoutAuthDevice(id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure API key authorization: cookie
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = AuthenticationApi();
|
||||||
|
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||||
|
|
||||||
|
try {
|
||||||
|
api_instance.logoutAuthDevice(id);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AuthenticationApi->logoutAuthDevice: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**id** | **String**| |
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
void (empty response body)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: Not defined
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **logoutAuthDevices**
|
||||||
|
> logoutAuthDevices()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure API key authorization: cookie
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = AuthenticationApi();
|
||||||
|
|
||||||
|
try {
|
||||||
|
api_instance.logoutAuthDevices();
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling AuthenticationApi->logoutAuthDevices: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
This endpoint does not need any parameter.
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
void (empty response body)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: Not defined
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **signUpAdmin**
|
# **signUpAdmin**
|
||||||
> UserResponseDto signUpAdmin(signUpDto)
|
> UserResponseDto signUpAdmin(signUpDto)
|
||||||
|
|
||||||
|
|||||||
171
mobile/openapi/doc/SessionsApi.md
generated
171
mobile/openapi/doc/SessionsApi.md
generated
@@ -1,171 +0,0 @@
|
|||||||
# 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)
|
|
||||||
|
|
||||||
1
mobile/openapi/doc/SystemConfigImageDto.md
generated
1
mobile/openapi/doc/SystemConfigImageDto.md
generated
@@ -9,7 +9,6 @@ import 'package:openapi/api.dart';
|
|||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**colorspace** | [**Colorspace**](Colorspace.md) | |
|
**colorspace** | [**Colorspace**](Colorspace.md) | |
|
||||||
**extractEmbedded** | **bool** | |
|
|
||||||
**previewFormat** | [**ImageFormat**](ImageFormat.md) | |
|
**previewFormat** | [**ImageFormat**](ImageFormat.md) | |
|
||||||
**previewSize** | **int** | |
|
**previewSize** | **int** | |
|
||||||
**quality** | **int** | |
|
**quality** | **int** | |
|
||||||
|
|||||||
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
@@ -45,7 +45,6 @@ part 'api/partner_api.dart';
|
|||||||
part 'api/person_api.dart';
|
part 'api/person_api.dart';
|
||||||
part 'api/search_api.dart';
|
part 'api/search_api.dart';
|
||||||
part 'api/server_info_api.dart';
|
part 'api/server_info_api.dart';
|
||||||
part 'api/sessions_api.dart';
|
|
||||||
part 'api/shared_link_api.dart';
|
part 'api/shared_link_api.dart';
|
||||||
part 'api/sync_api.dart';
|
part 'api/sync_api.dart';
|
||||||
part 'api/system_config_api.dart';
|
part 'api/system_config_api.dart';
|
||||||
@@ -87,6 +86,7 @@ part 'model/asset_stats_response_dto.dart';
|
|||||||
part 'model/asset_type_enum.dart';
|
part 'model/asset_type_enum.dart';
|
||||||
part 'model/audio_codec.dart';
|
part 'model/audio_codec.dart';
|
||||||
part 'model/audit_deletes_response_dto.dart';
|
part 'model/audit_deletes_response_dto.dart';
|
||||||
|
part 'model/auth_device_response_dto.dart';
|
||||||
part 'model/bulk_id_response_dto.dart';
|
part 'model/bulk_id_response_dto.dart';
|
||||||
part 'model/bulk_ids_dto.dart';
|
part 'model/bulk_ids_dto.dart';
|
||||||
part 'model/clip_config.dart';
|
part 'model/clip_config.dart';
|
||||||
@@ -176,7 +176,6 @@ part 'model/server_ping_response.dart';
|
|||||||
part 'model/server_stats_response_dto.dart';
|
part 'model/server_stats_response_dto.dart';
|
||||||
part 'model/server_theme_dto.dart';
|
part 'model/server_theme_dto.dart';
|
||||||
part 'model/server_version_response_dto.dart';
|
part 'model/server_version_response_dto.dart';
|
||||||
part 'model/session_response_dto.dart';
|
|
||||||
part 'model/shared_link_create_dto.dart';
|
part 'model/shared_link_create_dto.dart';
|
||||||
part 'model/shared_link_edit_dto.dart';
|
part 'model/shared_link_edit_dto.dart';
|
||||||
part 'model/shared_link_response_dto.dart';
|
part 'model/shared_link_response_dto.dart';
|
||||||
|
|||||||
117
mobile/openapi/lib/api/authentication_api.dart
generated
117
mobile/openapi/lib/api/authentication_api.dart
generated
@@ -63,6 +63,50 @@ class AuthenticationApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /auth/devices' operation and returns the [Response].
|
||||||
|
Future<Response> getAuthDevicesWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/auth/devices';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AuthDeviceResponseDto>?> getAuthDevices() async {
|
||||||
|
final response = await getAuthDevicesWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<AuthDeviceResponseDto>') as List)
|
||||||
|
.cast<AuthDeviceResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
|
/// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
@@ -151,6 +195,79 @@ class AuthenticationApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'DELETE /auth/devices/{id}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<Response> logoutAuthDeviceWithHttpInfo(String id,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/auth/devices/{id}'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<void> logoutAuthDevice(String id,) async {
|
||||||
|
final response = await logoutAuthDeviceWithHttpInfo(id,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'DELETE /auth/devices' operation and returns the [Response].
|
||||||
|
Future<Response> logoutAuthDevicesWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/auth/devices';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logoutAuthDevices() async {
|
||||||
|
final response = await logoutAuthDevicesWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response].
|
/// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
135
mobile/openapi/lib/api/sessions_api.dart
generated
135
mobile/openapi/lib/api/sessions_api.dart
generated
@@ -1,135 +0,0 @@
|
|||||||
//
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@@ -248,6 +248,8 @@ class ApiClient {
|
|||||||
return AudioCodecTypeTransformer().decode(value);
|
return AudioCodecTypeTransformer().decode(value);
|
||||||
case 'AuditDeletesResponseDto':
|
case 'AuditDeletesResponseDto':
|
||||||
return AuditDeletesResponseDto.fromJson(value);
|
return AuditDeletesResponseDto.fromJson(value);
|
||||||
|
case 'AuthDeviceResponseDto':
|
||||||
|
return AuthDeviceResponseDto.fromJson(value);
|
||||||
case 'BulkIdResponseDto':
|
case 'BulkIdResponseDto':
|
||||||
return BulkIdResponseDto.fromJson(value);
|
return BulkIdResponseDto.fromJson(value);
|
||||||
case 'BulkIdsDto':
|
case 'BulkIdsDto':
|
||||||
@@ -426,8 +428,6 @@ class ApiClient {
|
|||||||
return ServerThemeDto.fromJson(value);
|
return ServerThemeDto.fromJson(value);
|
||||||
case 'ServerVersionResponseDto':
|
case 'ServerVersionResponseDto':
|
||||||
return ServerVersionResponseDto.fromJson(value);
|
return ServerVersionResponseDto.fromJson(value);
|
||||||
case 'SessionResponseDto':
|
|
||||||
return SessionResponseDto.fromJson(value);
|
|
||||||
case 'SharedLinkCreateDto':
|
case 'SharedLinkCreateDto':
|
||||||
return SharedLinkCreateDto.fromJson(value);
|
return SharedLinkCreateDto.fromJson(value);
|
||||||
case 'SharedLinkEditDto':
|
case 'SharedLinkEditDto':
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
part of openapi.api;
|
part of openapi.api;
|
||||||
|
|
||||||
class SessionResponseDto {
|
class AuthDeviceResponseDto {
|
||||||
/// Returns a new [SessionResponseDto] instance.
|
/// Returns a new [AuthDeviceResponseDto] instance.
|
||||||
SessionResponseDto({
|
AuthDeviceResponseDto({
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.current,
|
required this.current,
|
||||||
required this.deviceOS,
|
required this.deviceOS,
|
||||||
@@ -34,7 +34,7 @@ class SessionResponseDto {
|
|||||||
String updatedAt;
|
String updatedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SessionResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AuthDeviceResponseDto &&
|
||||||
other.createdAt == createdAt &&
|
other.createdAt == createdAt &&
|
||||||
other.current == current &&
|
other.current == current &&
|
||||||
other.deviceOS == deviceOS &&
|
other.deviceOS == deviceOS &&
|
||||||
@@ -53,7 +53,7 @@ class SessionResponseDto {
|
|||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]';
|
String toString() => 'AuthDeviceResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -66,14 +66,14 @@ class SessionResponseDto {
|
|||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new [SessionResponseDto] instance and imports its values from
|
/// Returns a new [AuthDeviceResponseDto] instance and imports its values from
|
||||||
/// [value] if it's a [Map], null otherwise.
|
/// [value] if it's a [Map], null otherwise.
|
||||||
// ignore: prefer_constructors_over_static_methods
|
// ignore: prefer_constructors_over_static_methods
|
||||||
static SessionResponseDto? fromJson(dynamic value) {
|
static AuthDeviceResponseDto? fromJson(dynamic value) {
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return SessionResponseDto(
|
return AuthDeviceResponseDto(
|
||||||
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
||||||
current: mapValueOfType<bool>(json, r'current')!,
|
current: mapValueOfType<bool>(json, r'current')!,
|
||||||
deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
|
deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
|
||||||
@@ -85,11 +85,11 @@ class SessionResponseDto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<SessionResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
static List<AuthDeviceResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final result = <SessionResponseDto>[];
|
final result = <AuthDeviceResponseDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
final value = SessionResponseDto.fromJson(row);
|
final value = AuthDeviceResponseDto.fromJson(row);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
result.add(value);
|
result.add(value);
|
||||||
}
|
}
|
||||||
@@ -98,12 +98,12 @@ class SessionResponseDto {
|
|||||||
return result.toList(growable: growable);
|
return result.toList(growable: growable);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, SessionResponseDto> mapFromJson(dynamic json) {
|
static Map<String, AuthDeviceResponseDto> mapFromJson(dynamic json) {
|
||||||
final map = <String, SessionResponseDto>{};
|
final map = <String, AuthDeviceResponseDto>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = SessionResponseDto.fromJson(entry.value);
|
final value = AuthDeviceResponseDto.fromJson(entry.value);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@@ -112,14 +112,14 @@ class SessionResponseDto {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of SessionResponseDto-objects as value to a dart map
|
// maps a json object with a list of AuthDeviceResponseDto-objects as value to a dart map
|
||||||
static Map<String, List<SessionResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
static Map<String, List<AuthDeviceResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final map = <String, List<SessionResponseDto>>{};
|
final map = <String, List<AuthDeviceResponseDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
json = json.cast<String, dynamic>();
|
json = json.cast<String, dynamic>();
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
map[entry.key] = SessionResponseDto.listFromJson(entry.value, growable: growable,);
|
map[entry.key] = AuthDeviceResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@@ -14,7 +14,6 @@ class SystemConfigImageDto {
|
|||||||
/// Returns a new [SystemConfigImageDto] instance.
|
/// Returns a new [SystemConfigImageDto] instance.
|
||||||
SystemConfigImageDto({
|
SystemConfigImageDto({
|
||||||
required this.colorspace,
|
required this.colorspace,
|
||||||
required this.extractEmbedded,
|
|
||||||
required this.previewFormat,
|
required this.previewFormat,
|
||||||
required this.previewSize,
|
required this.previewSize,
|
||||||
required this.quality,
|
required this.quality,
|
||||||
@@ -24,8 +23,6 @@ class SystemConfigImageDto {
|
|||||||
|
|
||||||
Colorspace colorspace;
|
Colorspace colorspace;
|
||||||
|
|
||||||
bool extractEmbedded;
|
|
||||||
|
|
||||||
ImageFormat previewFormat;
|
ImageFormat previewFormat;
|
||||||
|
|
||||||
int previewSize;
|
int previewSize;
|
||||||
@@ -39,7 +36,6 @@ class SystemConfigImageDto {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto &&
|
||||||
other.colorspace == colorspace &&
|
other.colorspace == colorspace &&
|
||||||
other.extractEmbedded == extractEmbedded &&
|
|
||||||
other.previewFormat == previewFormat &&
|
other.previewFormat == previewFormat &&
|
||||||
other.previewSize == previewSize &&
|
other.previewSize == previewSize &&
|
||||||
other.quality == quality &&
|
other.quality == quality &&
|
||||||
@@ -50,7 +46,6 @@ class SystemConfigImageDto {
|
|||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(colorspace.hashCode) +
|
(colorspace.hashCode) +
|
||||||
(extractEmbedded.hashCode) +
|
|
||||||
(previewFormat.hashCode) +
|
(previewFormat.hashCode) +
|
||||||
(previewSize.hashCode) +
|
(previewSize.hashCode) +
|
||||||
(quality.hashCode) +
|
(quality.hashCode) +
|
||||||
@@ -58,12 +53,11 @@ class SystemConfigImageDto {
|
|||||||
(thumbnailSize.hashCode);
|
(thumbnailSize.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]';
|
String toString() => 'SystemConfigImageDto[colorspace=$colorspace, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'colorspace'] = this.colorspace;
|
json[r'colorspace'] = this.colorspace;
|
||||||
json[r'extractEmbedded'] = this.extractEmbedded;
|
|
||||||
json[r'previewFormat'] = this.previewFormat;
|
json[r'previewFormat'] = this.previewFormat;
|
||||||
json[r'previewSize'] = this.previewSize;
|
json[r'previewSize'] = this.previewSize;
|
||||||
json[r'quality'] = this.quality;
|
json[r'quality'] = this.quality;
|
||||||
@@ -81,7 +75,6 @@ class SystemConfigImageDto {
|
|||||||
|
|
||||||
return SystemConfigImageDto(
|
return SystemConfigImageDto(
|
||||||
colorspace: Colorspace.fromJson(json[r'colorspace'])!,
|
colorspace: Colorspace.fromJson(json[r'colorspace'])!,
|
||||||
extractEmbedded: mapValueOfType<bool>(json, r'extractEmbedded')!,
|
|
||||||
previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!,
|
previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!,
|
||||||
previewSize: mapValueOfType<int>(json, r'previewSize')!,
|
previewSize: mapValueOfType<int>(json, r'previewSize')!,
|
||||||
quality: mapValueOfType<int>(json, r'quality')!,
|
quality: mapValueOfType<int>(json, r'quality')!,
|
||||||
@@ -135,7 +128,6 @@ class SystemConfigImageDto {
|
|||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'colorspace',
|
'colorspace',
|
||||||
'extractEmbedded',
|
|
||||||
'previewFormat',
|
'previewFormat',
|
||||||
'previewSize',
|
'previewSize',
|
||||||
'quality',
|
'quality',
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
// tests for SessionResponseDto
|
// tests for AuthDeviceResponseDto
|
||||||
void main() {
|
void main() {
|
||||||
// final instance = SessionResponseDto();
|
// final instance = AuthDeviceResponseDto();
|
||||||
|
|
||||||
group('test SessionResponseDto', () {
|
group('test AuthDeviceResponseDto', () {
|
||||||
// String createdAt
|
// String createdAt
|
||||||
test('to test the property `createdAt`', () async {
|
test('to test the property `createdAt`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
15
mobile/openapi/test/authentication_api_test.dart
generated
15
mobile/openapi/test/authentication_api_test.dart
generated
@@ -22,6 +22,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Future<List<AuthDeviceResponseDto>> getAuthDevices() async
|
||||||
|
test('test getAuthDevices', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
//Future<LoginResponseDto> login(LoginCredentialDto loginCredentialDto) async
|
//Future<LoginResponseDto> login(LoginCredentialDto loginCredentialDto) async
|
||||||
test('test login', () async {
|
test('test login', () async {
|
||||||
// TODO
|
// TODO
|
||||||
@@ -32,6 +37,16 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Future logoutAuthDevice(String id) async
|
||||||
|
test('test logoutAuthDevice', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
//Future logoutAuthDevices() async
|
||||||
|
test('test logoutAuthDevices', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
//Future<UserResponseDto> signUpAdmin(SignUpDto signUpDto) async
|
//Future<UserResponseDto> signUpAdmin(SignUpDto signUpDto) async
|
||||||
test('test signUpAdmin', () async {
|
test('test signUpAdmin', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|||||||
36
mobile/openapi/test/sessions_api_test.dart
generated
36
mobile/openapi/test/sessions_api_test.dart
generated
@@ -1,36 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -21,11 +21,6 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
// bool extractEmbedded
|
|
||||||
test('to test the property `extractEmbedded`', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
// ImageFormat previewFormat
|
// ImageFormat previewFormat
|
||||||
test('to test the property `previewFormat`', () async {
|
test('to test the property `previewFormat`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.102.0+132
|
version: 1.101.0+131
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@@ -105,6 +105,9 @@ flutter:
|
|||||||
- assets/
|
- assets/
|
||||||
- assets/i18n/
|
- assets/i18n/
|
||||||
fonts:
|
fonts:
|
||||||
|
- family: SnowburstOne
|
||||||
|
fonts:
|
||||||
|
- asset: fonts/SnowburstOne.ttf
|
||||||
- family: Inconsolata
|
- family: Inconsolata
|
||||||
fonts:
|
fonts:
|
||||||
- asset: fonts/Inconsolata-Regular.ttf
|
- asset: fonts/Inconsolata-Regular.ttf
|
||||||
|
|||||||
@@ -2530,6 +2530,99 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth/devices": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "logoutAuthDevices",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "getAuthDevices",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AuthDeviceResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/devices/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "logoutAuthDevice",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/login": {
|
"/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "login",
|
"operationId": "login",
|
||||||
@@ -5091,99 +5184,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/sessions": {
|
|
||||||
"delete": {
|
|
||||||
"operationId": "deleteAllSessions",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Sessions"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"get": {
|
|
||||||
"operationId": "getSessions",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/SessionResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Sessions"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/sessions/{id}": {
|
|
||||||
"delete": {
|
|
||||||
"operationId": "deleteSession",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Sessions"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/shared-link": {
|
"/shared-link": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAllSharedLinks",
|
"operationId": "getAllSharedLinks",
|
||||||
@@ -7006,7 +7006,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@@ -7892,6 +7892,37 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"AuthDeviceResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"current": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"deviceOS": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deviceType": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"createdAt",
|
||||||
|
"current",
|
||||||
|
"deviceOS",
|
||||||
|
"deviceType",
|
||||||
|
"id",
|
||||||
|
"updatedAt"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"BulkIdResponseDto": {
|
"BulkIdResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"error": {
|
"error": {
|
||||||
@@ -10018,37 +10049,6 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"SessionResponseDto": {
|
|
||||||
"properties": {
|
|
||||||
"createdAt": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"current": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"deviceOS": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"deviceType": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"createdAt",
|
|
||||||
"current",
|
|
||||||
"deviceOS",
|
|
||||||
"deviceType",
|
|
||||||
"id",
|
|
||||||
"updatedAt"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"SharedLinkCreateDto": {
|
"SharedLinkCreateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"albumId": {
|
"albumId": {
|
||||||
@@ -10531,9 +10531,6 @@
|
|||||||
"colorspace": {
|
"colorspace": {
|
||||||
"$ref": "#/components/schemas/Colorspace"
|
"$ref": "#/components/schemas/Colorspace"
|
||||||
},
|
},
|
||||||
"extractEmbedded": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"previewFormat": {
|
"previewFormat": {
|
||||||
"$ref": "#/components/schemas/ImageFormat"
|
"$ref": "#/components/schemas/ImageFormat"
|
||||||
},
|
},
|
||||||
@@ -10552,7 +10549,6 @@
|
|||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"colorspace",
|
"colorspace",
|
||||||
"extractEmbedded",
|
|
||||||
"previewFormat",
|
"previewFormat",
|
||||||
"previewSize",
|
"previewSize",
|
||||||
"quality",
|
"quality",
|
||||||
|
|||||||
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.102.0
|
* 1.101.0
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
@@ -346,6 +346,14 @@ export type ChangePasswordDto = {
|
|||||||
newPassword: string;
|
newPassword: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
export type AuthDeviceResponseDto = {
|
||||||
|
createdAt: string;
|
||||||
|
current: boolean;
|
||||||
|
deviceOS: string;
|
||||||
|
deviceType: string;
|
||||||
|
id: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
export type LoginCredentialDto = {
|
export type LoginCredentialDto = {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -783,14 +791,6 @@ export type ServerVersionResponseDto = {
|
|||||||
minor: number;
|
minor: number;
|
||||||
patch: number;
|
patch: number;
|
||||||
};
|
};
|
||||||
export type SessionResponseDto = {
|
|
||||||
createdAt: string;
|
|
||||||
current: boolean;
|
|
||||||
deviceOS: string;
|
|
||||||
deviceType: string;
|
|
||||||
id: string;
|
|
||||||
updatedAt: string;
|
|
||||||
};
|
|
||||||
export type SharedLinkResponseDto = {
|
export type SharedLinkResponseDto = {
|
||||||
album?: AlbumResponseDto;
|
album?: AlbumResponseDto;
|
||||||
allowDownload: boolean;
|
allowDownload: boolean;
|
||||||
@@ -864,7 +864,6 @@ export type SystemConfigFFmpegDto = {
|
|||||||
};
|
};
|
||||||
export type SystemConfigImageDto = {
|
export type SystemConfigImageDto = {
|
||||||
colorspace: Colorspace;
|
colorspace: Colorspace;
|
||||||
extractEmbedded: boolean;
|
|
||||||
previewFormat: ImageFormat;
|
previewFormat: ImageFormat;
|
||||||
previewSize: number;
|
previewSize: number;
|
||||||
quality: number;
|
quality: number;
|
||||||
@@ -1704,6 +1703,28 @@ export function changePassword({ changePasswordDto }: {
|
|||||||
body: changePasswordDto
|
body: changePasswordDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
export function logoutAuthDevices(opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/auth/devices", {
|
||||||
|
...opts,
|
||||||
|
method: "DELETE"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function getAuthDevices(opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: AuthDeviceResponseDto[];
|
||||||
|
}>("/auth/devices", {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function logoutAuthDevice({ id }: {
|
||||||
|
id: string;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText(`/auth/devices/${encodeURIComponent(id)}`, {
|
||||||
|
...opts,
|
||||||
|
method: "DELETE"
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function login({ loginCredentialDto }: {
|
export function login({ loginCredentialDto }: {
|
||||||
loginCredentialDto: LoginCredentialDto;
|
loginCredentialDto: LoginCredentialDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
@@ -2392,28 +2413,6 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function deleteAllSessions(opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchText("/sessions", {
|
|
||||||
...opts,
|
|
||||||
method: "DELETE"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function getSessions(opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: SessionResponseDto[];
|
|
||||||
}>("/sessions", {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function deleteSession({ id }: {
|
|
||||||
id: string;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchText(`/sessions/${encodeURIComponent(id)}`, {
|
|
||||||
...opts,
|
|
||||||
method: "DELETE"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function getAllSharedLinks(opts?: Oazapfts.RequestOpts) {
|
export function getAllSharedLinks(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:20240416@sha256:ff2aadf54298e8ceca94031c6fed143236d8d82640fbbf422e0a9d2978e45923 as dev
|
FROM ghcr.io/immich-app/base-server-dev:20240326@sha256:d945aba864051b30888617f36446f86b72c4bc7ad6476b9dd2aaa0c4c4e3c945 as dev
|
||||||
|
|
||||||
RUN apt-get install --no-install-recommends -yqq tini
|
RUN apt-get install --no-install-recommends -yqq tini
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
|
|||||||
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
|
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
|
||||||
|
|
||||||
# web build
|
# web build
|
||||||
FROM node:iron-alpine3.18@sha256:d328c7bc3305e1ab26491817936c8151a47a8861ad617c16c1eeaa9c8075c8f6 as web
|
FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e as web
|
||||||
|
|
||||||
WORKDIR /usr/src/open-api/typescript-sdk
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
@@ -41,7 +41,7 @@ RUN npm run build
|
|||||||
|
|
||||||
|
|
||||||
# prod build
|
# prod build
|
||||||
FROM ghcr.io/immich-app/base-server-prod:20240416@sha256:138f4d6fb74b282256583070339eaba6f39fcffa3569ae05b6823d5c37098242
|
FROM ghcr.io/immich-app/base-server-prod:20240326@sha256:28ad98fed8d746b5f92de49ff776cfdff7399df163ebeda2f37a01f473965841
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|||||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^10.0.1",
|
"@nestjs/bullmq": "^10.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.102.0",
|
"version": "1.101.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
@@ -12,7 +13,16 @@ export class ListUsersCommand extends CommandRunner {
|
|||||||
|
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const users = await this.userService.listUsers();
|
const users = await this.userService.getAll(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
id: 'cli',
|
||||||
|
email: 'cli@immich.app',
|
||||||
|
isAdmin: true,
|
||||||
|
} as UserEntity,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
console.dir(users);
|
console.dir(users);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
|
|||||||
|
|
||||||
export const MOBILE_REDIRECT = 'app.immich:/';
|
export const MOBILE_REDIRECT = 'app.immich:/';
|
||||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||||
|
export const IMMICH_ACCESS_COOKIE = 'immich_access_token';
|
||||||
|
export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated';
|
||||||
|
export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
|
||||||
|
export const IMMICH_API_KEY_NAME = 'api_key';
|
||||||
|
export const IMMICH_API_KEY_HEADER = 'x-api-key';
|
||||||
|
export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token';
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
PASSWORD = 'password',
|
PASSWORD = 'password',
|
||||||
OAUTH = 'oauth',
|
OAUTH = 'oauth',
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AuthType } from 'src/constants';
|
import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants';
|
||||||
import {
|
import {
|
||||||
|
AuthDeviceResponseDto,
|
||||||
AuthDto,
|
AuthDto,
|
||||||
ChangePasswordDto,
|
ChangePasswordDto,
|
||||||
ImmichCookie,
|
|
||||||
LoginCredentialDto,
|
LoginCredentialDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
LogoutResponseDto,
|
LogoutResponseDto,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||||
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Authentication')
|
@ApiTags('Authentication')
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@@ -30,15 +30,9 @@ export class AuthController {
|
|||||||
@Res({ passthrough: true }) res: Response,
|
@Res({ passthrough: true }) res: Response,
|
||||||
@GetLoginDetails() loginDetails: LoginDetails,
|
@GetLoginDetails() loginDetails: LoginDetails,
|
||||||
): Promise<LoginResponseDto> {
|
): Promise<LoginResponseDto> {
|
||||||
const body = await this.service.login(loginCredential, loginDetails);
|
const { response, cookie } = await this.service.login(loginCredential, loginDetails);
|
||||||
return respondWithCookie(res, body, {
|
res.header('Set-Cookie', cookie);
|
||||||
isSecure: loginDetails.isSecure,
|
return response;
|
||||||
values: [
|
|
||||||
{ key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken },
|
|
||||||
{ key: ImmichCookie.AUTH_TYPE, value: AuthType.PASSWORD },
|
|
||||||
{ key: ImmichCookie.IS_AUTHENTICATED, value: 'true' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PublicRoute()
|
@PublicRoute()
|
||||||
@@ -47,6 +41,23 @@ export class AuthController {
|
|||||||
return this.service.adminSignUp(dto);
|
return this.service.adminSignUp(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('devices')
|
||||||
|
getAuthDevices(@Auth() auth: AuthDto): Promise<AuthDeviceResponseDto[]> {
|
||||||
|
return this.service.getDevices(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('devices')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
logoutAuthDevices(@Auth() auth: AuthDto): Promise<void> {
|
||||||
|
return this.service.logoutDevices(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('devices/:id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
logoutAuthDevice(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
|
return this.service.logoutDevice(auth, id);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('validateToken')
|
@Post('validateToken')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
validateAccessToken(): ValidateAccessTokenResponseDto {
|
validateAccessToken(): ValidateAccessTokenResponseDto {
|
||||||
@@ -61,18 +72,15 @@ export class AuthController {
|
|||||||
|
|
||||||
@Post('logout')
|
@Post('logout')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
async logout(
|
logout(
|
||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@Res({ passthrough: true }) res: Response,
|
@Res({ passthrough: true }) res: Response,
|
||||||
@Auth() auth: AuthDto,
|
@Auth() auth: AuthDto,
|
||||||
): Promise<LogoutResponseDto> {
|
): Promise<LogoutResponseDto> {
|
||||||
const authType = (request.cookies || {})[ImmichCookie.AUTH_TYPE];
|
res.clearCookie(IMMICH_ACCESS_COOKIE);
|
||||||
|
res.clearCookie(IMMICH_AUTH_TYPE_COOKIE);
|
||||||
|
res.clearCookie(IMMICH_IS_AUTHENTICATED);
|
||||||
|
|
||||||
const body = await this.service.logout(auth, authType);
|
return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]);
|
||||||
return respondWithoutCookie(res, body, [
|
|
||||||
ImmichCookie.ACCESS_TOKEN,
|
|
||||||
ImmichCookie.AUTH_TYPE,
|
|
||||||
ImmichCookie.IS_AUTHENTICATED,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import { MemoryController } from 'src/controllers/memory.controller';
|
|||||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
import { PersonController } from 'src/controllers/person.controller';
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
|
import { PluginController } from 'src/controllers/plugin.controller';
|
||||||
import { SearchController } from 'src/controllers/search.controller';
|
import { SearchController } from 'src/controllers/search.controller';
|
||||||
import { ServerInfoController } from 'src/controllers/server-info.controller';
|
import { ServerInfoController } from 'src/controllers/server-info.controller';
|
||||||
import { SessionController } from 'src/controllers/session.controller';
|
|
||||||
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
||||||
import { SyncController } from 'src/controllers/sync.controller';
|
import { SyncController } from 'src/controllers/sync.controller';
|
||||||
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
||||||
@@ -42,9 +42,9 @@ export const controllers = [
|
|||||||
MemoryController,
|
MemoryController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
PartnerController,
|
PartnerController,
|
||||||
|
PluginController,
|
||||||
SearchController,
|
SearchController,
|
||||||
ServerInfoController,
|
ServerInfoController,
|
||||||
SessionController,
|
|
||||||
SharedLinkController,
|
SharedLinkController,
|
||||||
SyncController,
|
SyncController,
|
||||||
SystemConfigController,
|
SystemConfigController,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AuthType } from 'src/constants';
|
|
||||||
import {
|
import {
|
||||||
AuthDto,
|
AuthDto,
|
||||||
ImmichCookie,
|
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
OAuthAuthorizeResponseDto,
|
OAuthAuthorizeResponseDto,
|
||||||
OAuthCallbackDto,
|
OAuthCallbackDto,
|
||||||
@@ -13,7 +11,6 @@ import {
|
|||||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||||
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
import { respondWithCookie } from 'src/utils/response';
|
|
||||||
|
|
||||||
@ApiTags('OAuth')
|
@ApiTags('OAuth')
|
||||||
@Controller('oauth')
|
@Controller('oauth')
|
||||||
@@ -44,15 +41,9 @@ export class OAuthController {
|
|||||||
@Body() dto: OAuthCallbackDto,
|
@Body() dto: OAuthCallbackDto,
|
||||||
@GetLoginDetails() loginDetails: LoginDetails,
|
@GetLoginDetails() loginDetails: LoginDetails,
|
||||||
): Promise<LoginResponseDto> {
|
): Promise<LoginResponseDto> {
|
||||||
const body = await this.service.callback(dto, loginDetails);
|
const { response, cookie } = await this.service.callback(dto, loginDetails);
|
||||||
return respondWithCookie(res, body, {
|
res.header('Set-Cookie', cookie);
|
||||||
isSecure: loginDetails.isSecure,
|
return response;
|
||||||
values: [
|
|
||||||
{ key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken },
|
|
||||||
{ key: ImmichCookie.AUTH_TYPE, value: AuthType.OAUTH },
|
|
||||||
{ key: ImmichCookie.IS_AUTHENTICATED, value: 'true' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('link')
|
@Post('link')
|
||||||
|
|||||||
49
server/src/controllers/plugin.controller.ts
Normal file
49
server/src/controllers/plugin.controller.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { PluginImportDto, PluginResponseDto, PluginUpdateDto, SearchPluginDto } from 'src/dtos/plugin.dto';
|
||||||
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { PluginService } from 'src/services/plugin.service';
|
||||||
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
|
@ApiTags('Plugin')
|
||||||
|
@Controller('plugins')
|
||||||
|
@Authenticated()
|
||||||
|
export class PluginController {
|
||||||
|
constructor(private service: PluginService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
searchPlugins(@Auth() auth: AuthDto, @Query() dto: SearchPluginDto): Promise<PluginResponseDto[]> {
|
||||||
|
return this.service.search(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
importPlugin(@Auth() auth: AuthDto, @Body() dto: PluginImportDto): Promise<PluginResponseDto> {
|
||||||
|
return this.service.import(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
updatePlugin(
|
||||||
|
@Auth() auth: AuthDto,
|
||||||
|
@Param() { id }: UUIDParamDto,
|
||||||
|
@Body() dto: PluginUpdateDto,
|
||||||
|
): Promise<PluginResponseDto> {
|
||||||
|
return this.service.update(auth, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/install')
|
||||||
|
installPlugin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PluginResponseDto> {
|
||||||
|
return this.service.install(auth, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/uninstall')
|
||||||
|
uninstallPlugin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PluginResponseDto> {
|
||||||
|
return this.service.uninstall(auth, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
deletePlugin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
|
return this.service.delete(auth, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants';
|
||||||
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||||
import { AuthDto, ImmichCookie } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
SharedLinkEditDto,
|
SharedLinkEditDto,
|
||||||
SharedLinkPasswordDto,
|
SharedLinkPasswordDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
} from 'src/dtos/shared-link.dto';
|
} from 'src/dtos/shared-link.dto';
|
||||||
import { Auth, Authenticated, GetLoginDetails, SharedLinkRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
import { LoginDetails } from 'src/services/auth.service';
|
|
||||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
import { respondWithCookie } from 'src/utils/response';
|
|
||||||
import { UUIDParamDto } from 'src/validation';
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
@ApiTags('Shared Link')
|
@ApiTags('Shared Link')
|
||||||
@@ -34,17 +33,20 @@ export class SharedLinkController {
|
|||||||
@Query() dto: SharedLinkPasswordDto,
|
@Query() dto: SharedLinkPasswordDto,
|
||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@Res({ passthrough: true }) res: Response,
|
@Res({ passthrough: true }) res: Response,
|
||||||
@GetLoginDetails() loginDetails: LoginDetails,
|
|
||||||
): Promise<SharedLinkResponseDto> {
|
): Promise<SharedLinkResponseDto> {
|
||||||
const sharedLinkToken = request.cookies?.[ImmichCookie.SHARED_LINK_TOKEN];
|
const sharedLinkToken = request.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE];
|
||||||
if (sharedLinkToken) {
|
if (sharedLinkToken) {
|
||||||
dto.token = sharedLinkToken;
|
dto.token = sharedLinkToken;
|
||||||
}
|
}
|
||||||
const body = await this.service.getMine(auth, dto);
|
const response = await this.service.getMine(auth, dto);
|
||||||
return respondWithCookie(res, body, {
|
if (response.token) {
|
||||||
isSecure: loginDetails.isSecure,
|
res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, response.token, {
|
||||||
values: body.token ? [{ key: ImmichCookie.SHARED_LINK_TOKEN, value: body.token }] : [],
|
expires: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
||||||
});
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ export enum Permission {
|
|||||||
PERSON_CREATE = 'person.create',
|
PERSON_CREATE = 'person.create',
|
||||||
PERSON_REASSIGN = 'person.reassign',
|
PERSON_REASSIGN = 'person.reassign',
|
||||||
|
|
||||||
|
PLUGIN_READ = 'plugin.read',
|
||||||
|
PLUGIN_WRITE = 'plugin.write',
|
||||||
|
PLUGIN_DELETE = 'plugin.delete',
|
||||||
|
PLUGIN_ADMIN = 'plugin.admin',
|
||||||
|
PLUGIN_INSTALL = 'plugin.install',
|
||||||
|
PLUGIN_UNINSTALL = 'plugin.uninstall',
|
||||||
|
|
||||||
PARTNER_UPDATE = 'partner.update',
|
PARTNER_UPDATE = 'partner.update',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
|
||||||
import { dirname, join, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
import { APP_MEDIA_LOCATION } from 'src/constants';
|
import { APP_MEDIA_LOCATION } from 'src/constants';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
@@ -309,8 +308,4 @@ export class StorageCore {
|
|||||||
static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string {
|
static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string {
|
||||||
return join(this.getNestedFolder(folder, ownerId, filename), filename);
|
return join(this.getNestedFolder(folder, ownerId, filename), filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getTempPathInDir(dir: string): string {
|
|
||||||
return join(dir, `${randomUUID()}.tmp`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
previewSize: 1440,
|
previewSize: 1440,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
extractEmbedded: false,
|
|
||||||
},
|
},
|
||||||
newVersionCheck: {
|
newVersionCheck: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -2,35 +2,16 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||||
import { SessionEntity } from 'src/entities/session.entity';
|
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
|
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
|
|
||||||
export enum ImmichCookie {
|
|
||||||
ACCESS_TOKEN = 'immich_access_token',
|
|
||||||
AUTH_TYPE = 'immich_auth_type',
|
|
||||||
IS_AUTHENTICATED = 'immich_is_authenticated',
|
|
||||||
SHARED_LINK_TOKEN = 'immich_shared_link_token',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ImmichHeader {
|
|
||||||
API_KEY = 'x-api-key',
|
|
||||||
USER_TOKEN = 'x-immich-user-token',
|
|
||||||
SESSION_TOKEN = 'x-immich-session-token',
|
|
||||||
SHARED_LINK_TOKEN = 'x-immich-share-key',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CookieResponse = {
|
|
||||||
isSecure: boolean;
|
|
||||||
values: Array<{ key: ImmichCookie; value: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AuthDto {
|
export class AuthDto {
|
||||||
user!: UserEntity;
|
user!: UserEntity;
|
||||||
|
|
||||||
apiKey?: APIKeyEntity;
|
apiKey?: APIKeyEntity;
|
||||||
sharedLink?: SharedLinkEntity;
|
sharedLink?: SharedLinkEntity;
|
||||||
session?: SessionEntity;
|
userToken?: UserTokenEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoginCredentialDto {
|
export class LoginCredentialDto {
|
||||||
@@ -58,7 +39,7 @@ export class LoginResponseDto {
|
|||||||
|
|
||||||
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
|
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken: accessToken,
|
||||||
userId: entity.id,
|
userId: entity.id,
|
||||||
userEmail: entity.email,
|
userEmail: entity.email,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
@@ -97,6 +78,24 @@ export class ValidateAccessTokenResponseDto {
|
|||||||
authStatus!: boolean;
|
authStatus!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AuthDeviceResponseDto {
|
||||||
|
id!: string;
|
||||||
|
createdAt!: string;
|
||||||
|
updatedAt!: string;
|
||||||
|
current!: boolean;
|
||||||
|
deviceType!: string;
|
||||||
|
deviceOS!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({
|
||||||
|
id: entity.id,
|
||||||
|
createdAt: entity.createdAt.toISOString(),
|
||||||
|
updatedAt: entity.updatedAt.toISOString(),
|
||||||
|
current: currentId === entity.id,
|
||||||
|
deviceOS: entity.deviceOS,
|
||||||
|
deviceType: entity.deviceType,
|
||||||
|
});
|
||||||
|
|
||||||
export class OAuthCallbackDto {
|
export class OAuthCallbackDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
58
server/src/dtos/plugin.dto.ts
Normal file
58
server/src/dtos/plugin.dto.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsString } from 'class-validator';
|
||||||
|
import { PluginEntity } from 'src/entities/plugin.entity';
|
||||||
|
import { Optional, ValidateBoolean } from 'src/validation';
|
||||||
|
|
||||||
|
export class SearchPluginDto {
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
isEnabled?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
isOfficial?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
isInstalled?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Optional()
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginImportDto {
|
||||||
|
url!: string;
|
||||||
|
install!: boolean;
|
||||||
|
isEnabled!: boolean;
|
||||||
|
isOfficial!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginUpdateDto {
|
||||||
|
@IsBoolean()
|
||||||
|
isEnabled!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginResponseDto {
|
||||||
|
id!: string;
|
||||||
|
createdAt!: Date;
|
||||||
|
updatedAt!: Date;
|
||||||
|
packageId!: string;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
version!: number;
|
||||||
|
name!: string;
|
||||||
|
description!: string;
|
||||||
|
isEnabled!: boolean;
|
||||||
|
isInstalled!: boolean;
|
||||||
|
isTrusted!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapPlugin = (plugin: PluginEntity): PluginResponseDto => ({
|
||||||
|
id: plugin.id,
|
||||||
|
createdAt: plugin.createdAt,
|
||||||
|
updatedAt: plugin.updatedAt,
|
||||||
|
packageId: plugin.packageId,
|
||||||
|
version: plugin.version,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
isEnabled: plugin.isEnabled,
|
||||||
|
isInstalled: plugin.isInstalled,
|
||||||
|
isTrusted: plugin.isTrusted,
|
||||||
|
});
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
@@ -417,9 +417,6 @@ class SystemConfigImageDto {
|
|||||||
@IsEnum(Colorspace)
|
@IsEnum(Colorspace)
|
||||||
@ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
|
@ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
|
||||||
colorspace!: Colorspace;
|
colorspace!: Colorspace;
|
||||||
|
|
||||||
@ValidateBoolean()
|
|
||||||
extractEmbedded!: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemConfigTrashDto {
|
class SystemConfigTrashDto {
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import { MemoryEntity } from 'src/entities/memory.entity';
|
|||||||
import { MoveEntity } from 'src/entities/move.entity';
|
import { MoveEntity } from 'src/entities/move.entity';
|
||||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { SessionEntity } from 'src/entities/session.entity';
|
import { PluginEntity } from 'src/entities/plugin.entity';
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||||
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
||||||
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
||||||
import { TagEntity } from 'src/entities/tag.entity';
|
import { TagEntity } from 'src/entities/tag.entity';
|
||||||
|
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
|
|
||||||
export const entities = [
|
export const entities = [
|
||||||
@@ -37,6 +38,7 @@ export const entities = [
|
|||||||
MoveEntity,
|
MoveEntity,
|
||||||
PartnerEntity,
|
PartnerEntity,
|
||||||
PersonEntity,
|
PersonEntity,
|
||||||
|
PluginEntity,
|
||||||
SharedLinkEntity,
|
SharedLinkEntity,
|
||||||
SmartInfoEntity,
|
SmartInfoEntity,
|
||||||
SmartSearchEntity,
|
SmartSearchEntity,
|
||||||
@@ -44,6 +46,6 @@ export const entities = [
|
|||||||
SystemMetadataEntity,
|
SystemMetadataEntity,
|
||||||
TagEntity,
|
TagEntity,
|
||||||
UserEntity,
|
UserEntity,
|
||||||
SessionEntity,
|
UserTokenEntity,
|
||||||
LibraryEntity,
|
LibraryEntity,
|
||||||
];
|
];
|
||||||
|
|||||||
40
server/src/entities/plugin.entity.ts
Normal file
40
server/src/entities/plugin.entity.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('plugins')
|
||||||
|
export class PluginEntity {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn({ type: 'timestamptz', nullable: true })
|
||||||
|
deletedAt!: Date | null;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
packageId!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
version!: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
isEnabled!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isInstalled!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isTrusted!: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
installPath!: string | null;
|
||||||
|
}
|
||||||
@@ -114,7 +114,6 @@ export const SystemConfigKey = {
|
|||||||
IMAGE_PREVIEW_SIZE: 'image.previewSize',
|
IMAGE_PREVIEW_SIZE: 'image.previewSize',
|
||||||
IMAGE_QUALITY: 'image.quality',
|
IMAGE_QUALITY: 'image.quality',
|
||||||
IMAGE_COLORSPACE: 'image.colorspace',
|
IMAGE_COLORSPACE: 'image.colorspace',
|
||||||
IMAGE_EXTRACT_EMBEDDED: 'image.extractEmbedded',
|
|
||||||
|
|
||||||
TRASH_ENABLED: 'trash.enabled',
|
TRASH_ENABLED: 'trash.enabled',
|
||||||
TRASH_DAYS: 'trash.days',
|
TRASH_DAYS: 'trash.days',
|
||||||
@@ -285,7 +284,6 @@ export interface SystemConfig {
|
|||||||
previewSize: number;
|
previewSize: number;
|
||||||
quality: number;
|
quality: number;
|
||||||
colorspace: Colorspace;
|
colorspace: Colorspace;
|
||||||
extractEmbedded: boolean;
|
|
||||||
};
|
};
|
||||||
newVersionCheck: {
|
newVersionCheck: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('sessions')
|
@Entity('user_token')
|
||||||
export class SessionEntity {
|
export class UserTokenEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@@ -89,6 +89,9 @@ export enum JobName {
|
|||||||
SIDECAR_DISCOVERY = 'sidecar-discovery',
|
SIDECAR_DISCOVERY = 'sidecar-discovery',
|
||||||
SIDECAR_SYNC = 'sidecar-sync',
|
SIDECAR_SYNC = 'sidecar-sync',
|
||||||
SIDECAR_WRITE = 'sidecar-write',
|
SIDECAR_WRITE = 'sidecar-write',
|
||||||
|
|
||||||
|
// workflows
|
||||||
|
WORKFLOW_TRIGGER = 'workflow-trigger',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
||||||
@@ -135,6 +138,17 @@ export interface IDeferrableJob extends IEntityJob {
|
|||||||
deferred?: boolean;
|
deferred?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum WorkflowTriggerType {
|
||||||
|
ASSET_UPLOAD = 'asset.upload',
|
||||||
|
ASSET_UPDATE = 'asset.update',
|
||||||
|
ASSET_TRASH = 'asset.trash',
|
||||||
|
ASSET_DELETE = 'asset.delete',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IWorkflowTriggerJob =
|
||||||
|
| { type: WorkflowTriggerType.ASSET_UPLOAD; data: { assetId: string } }
|
||||||
|
| { type: WorkflowTriggerType.ASSET_UPDATE; data: { asset2Id: string } };
|
||||||
|
|
||||||
export interface JobCounts {
|
export interface JobCounts {
|
||||||
active: number;
|
active: number;
|
||||||
completed: number;
|
completed: number;
|
||||||
@@ -216,7 +230,8 @@ export type JobItem =
|
|||||||
| { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob }
|
| { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob }
|
||||||
| { name: JobName.LIBRARY_DELETE; data: IEntityJob }
|
| { name: JobName.LIBRARY_DELETE; data: IEntityJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob }
|
| { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob };
|
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
|
||||||
|
| { name: JobName.WORKFLOW_TRIGGER; data: IWorkflowTriggerJob };
|
||||||
|
|
||||||
export enum JobStatus {
|
export enum JobStatus {
|
||||||
SUCCESS = 'success',
|
SUCCESS = 'success',
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ export interface VideoFormat {
|
|||||||
bitrate: number;
|
bitrate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageDimensions {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VideoInfo {
|
export interface VideoInfo {
|
||||||
format: VideoFormat;
|
format: VideoFormat;
|
||||||
videoStreams: VideoStreamInfo[];
|
videoStreams: VideoStreamInfo[];
|
||||||
@@ -75,11 +70,9 @@ export interface VideoCodecHWConfig extends VideoCodecSWConfig {
|
|||||||
|
|
||||||
export interface IMediaRepository {
|
export interface IMediaRepository {
|
||||||
// image
|
// image
|
||||||
extract(input: string, output: string): Promise<boolean>;
|
|
||||||
resize(input: string | Buffer, output: string, options: ResizeOptions): Promise<void>;
|
resize(input: string | Buffer, output: string, options: ResizeOptions): Promise<void>;
|
||||||
crop(input: string, options: CropOptions): Promise<Buffer>;
|
crop(input: string, options: CropOptions): Promise<Buffer>;
|
||||||
generateThumbhash(imagePath: string): Promise<Buffer>;
|
generateThumbhash(imagePath: string): Promise<Buffer>;
|
||||||
getImageDimensions(input: string): Promise<ImageDimensions>;
|
|
||||||
|
|
||||||
// video
|
// video
|
||||||
probe(input: string): Promise<VideoInfo>;
|
probe(input: string): Promise<VideoInfo>;
|
||||||
|
|||||||
116
server/src/interfaces/plugin.interface.ts
Normal file
116
server/src/interfaces/plugin.interface.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { PluginEntity } from 'src/entities/plugin.entity';
|
||||||
|
|
||||||
|
export const IPluginRepository = 'IPluginRepository';
|
||||||
|
|
||||||
|
export interface PluginSearchOptions {
|
||||||
|
id?: string;
|
||||||
|
namespace?: string;
|
||||||
|
version?: number;
|
||||||
|
name?: string;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
isInstalled?: boolean;
|
||||||
|
isOfficial?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPluginRepository {
|
||||||
|
search(options: PluginSearchOptions): Promise<PluginEntity[]>;
|
||||||
|
create(dto: Partial<PluginEntity>): Promise<PluginEntity>;
|
||||||
|
get(id: string): Promise<PluginEntity | null>;
|
||||||
|
update(dto: Partial<PluginEntity>): Promise<PluginEntity>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
|
||||||
|
download(url: string, downloadPath: string): Promise<void>;
|
||||||
|
load(pluginPath: string): Promise<PluginLike>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginFactory = {
|
||||||
|
register: () => MaybePromise<Plugin>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginLike = Plugin | PluginFactory | { default: Plugin } | { plugin: Plugin };
|
||||||
|
|
||||||
|
export interface Plugin<T extends PluginConfig | undefined = undefined> {
|
||||||
|
version: 1;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
actions: PluginAction<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginAction<T extends PluginConfig | undefined = undefined> = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
events?: EventType[];
|
||||||
|
config?: T;
|
||||||
|
} & (
|
||||||
|
| { type: ActionType.ASSET; onAction: OnAction<T, AssetDto> }
|
||||||
|
| { type: ActionType.ALBUM; onAction: OnAction<T, AlbumDto> }
|
||||||
|
| { type: ActionType.ALBUM_ASSET; onAction: OnAction<T, { asset: AssetDto; album: AlbumDto }> }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type OnAction<T extends PluginConfig | undefined, D = PluginActionData> = T extends undefined
|
||||||
|
? (ctx: PluginContext, data: D) => MaybePromise<void>
|
||||||
|
: (ctx: PluginContext, data: D, config: InferConfig<T>) => MaybePromise<void>;
|
||||||
|
|
||||||
|
export interface PluginContext {
|
||||||
|
updateAsset: (asset: { id: string; isArchived: boolean }) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginActionData = { data: { asset?: AssetDto; album?: AlbumDto } } & (
|
||||||
|
| { type: EventType.ASSET_UPLOAD; data: { asset: AssetDto } }
|
||||||
|
| { type: EventType.ASSET_UPDATE; data: { asset: AssetDto } }
|
||||||
|
| { type: EventType.ASSET_TRASH; data: { asset: AssetDto } }
|
||||||
|
| { type: EventType.ASSET_DELETE; data: { asset: AssetDto } }
|
||||||
|
| { type: EventType.ALBUM_CREATE; data: { album: AlbumDto } }
|
||||||
|
| { type: EventType.ALBUM_UPDATE; data: { album: AlbumDto } }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PluginConfig = Record<string, ConfigItem>;
|
||||||
|
|
||||||
|
export type ConfigItem = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
required?: boolean;
|
||||||
|
} & { [K in Types]: { type: K; default?: InferType<K> } }[Types];
|
||||||
|
|
||||||
|
export type InferType<T extends Types> = T extends 'string'
|
||||||
|
? string
|
||||||
|
: T extends 'date'
|
||||||
|
? Date
|
||||||
|
: T extends 'number'
|
||||||
|
? number
|
||||||
|
: T extends 'boolean'
|
||||||
|
? boolean
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type Types = 'string' | 'boolean' | 'number' | 'date';
|
||||||
|
type MaybePromise<T = void> = Promise<T> | T;
|
||||||
|
type IfRequired<T extends ConfigItem, Type> = T['required'] extends true ? Type : Type | undefined;
|
||||||
|
type InferConfig<T> = T extends PluginConfig
|
||||||
|
? {
|
||||||
|
[K in keyof T]: IfRequired<T[K], InferType<T[K]['type']>>;
|
||||||
|
}
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export enum ActionType {
|
||||||
|
ASSET = 'asset',
|
||||||
|
ALBUM = 'album',
|
||||||
|
ALBUM_ASSET = 'album-asset',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EventType {
|
||||||
|
ASSET_UPLOAD = 'asset.upload',
|
||||||
|
ASSET_UPDATE = 'asset.update',
|
||||||
|
ASSET_TRASH = 'asset.trash',
|
||||||
|
ASSET_DELETE = 'asset.delete',
|
||||||
|
ASSET_ARCHIVE = 'asset.archvie',
|
||||||
|
ASSET_UNARCHIVE = 'asset.unarchive',
|
||||||
|
|
||||||
|
ALBUM_CREATE = 'album.create',
|
||||||
|
ALBUM_UPDATE = 'album.update',
|
||||||
|
ALBUM_DELETE = 'album.delete',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssetDto = { id: string; type: 'asset' };
|
||||||
|
export type AlbumDto = { id: string; type: 'album' };
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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[]>;
|
|
||||||
}
|
|
||||||
11
server/src/interfaces/user-token.interface.ts
Normal file
11
server/src/interfaces/user-token.interface.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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[]>;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import { IMMICH_API_KEY_NAME } from 'src/constants';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||||
@@ -20,7 +21,6 @@ export enum Metadata {
|
|||||||
ADMIN_ROUTE = 'admin_route',
|
ADMIN_ROUTE = 'admin_route',
|
||||||
SHARED_ROUTE = 'shared_route',
|
SHARED_ROUTE = 'shared_route',
|
||||||
PUBLIC_SECURITY = 'public_security',
|
PUBLIC_SECURITY = 'public_security',
|
||||||
API_KEY_SECURITY = 'api_key',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticatedOptions {
|
export interface AuthenticatedOptions {
|
||||||
@@ -32,7 +32,7 @@ export const Authenticated = (options: AuthenticatedOptions = {}) => {
|
|||||||
const decorators: MethodDecorator[] = [
|
const decorators: MethodDecorator[] = [
|
||||||
ApiBearerAuth(),
|
ApiBearerAuth(),
|
||||||
ApiCookieAuth(),
|
ApiCookieAuth(),
|
||||||
ApiSecurity(Metadata.API_KEY_SECURITY),
|
ApiSecurity(IMMICH_API_KEY_NAME),
|
||||||
SetMetadata(Metadata.AUTH_ROUTE, true),
|
SetMetadata(Metadata.AUTH_ROUTE, true),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
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"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
55
server/src/plugins/asset.plugin.ts
Normal file
55
server/src/plugins/asset.plugin.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { ActionType, AssetDto, Plugin, PluginContext } from 'src/interfaces/plugin.interface';
|
||||||
|
|
||||||
|
const onAsset = async (ctx: PluginContext, asset: AssetDto) => {
|
||||||
|
await ctx.updateAsset({ id: asset.id, isArchived: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const plugin: Plugin = {
|
||||||
|
version: 1,
|
||||||
|
id: 'immich-plugin-asset',
|
||||||
|
name: 'Asset Plugin',
|
||||||
|
description: 'Immich asset plugin',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'asset.favorite',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: async (ctx, asset) => {
|
||||||
|
await ctx.updateAsset({ id: asset.id, isArchived: false });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'asset.unfavorite',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: () => {
|
||||||
|
console.log('Unfavorite');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'asset.action',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: (ctx, asset) => onAsset(ctx, asset),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'album-asset.action',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ALBUM_ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: (ctx, { asset }) => onAsset(ctx, asset),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'asset.unarchive',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: () => {
|
||||||
|
console.log('Unarchive');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -173,13 +173,13 @@ WHERE
|
|||||||
|
|
||||||
-- AccessRepository.authDevice.checkOwnerAccess
|
-- AccessRepository.authDevice.checkOwnerAccess
|
||||||
SELECT
|
SELECT
|
||||||
"SessionEntity"."id" AS "SessionEntity_id"
|
"UserTokenEntity"."id" AS "UserTokenEntity_id"
|
||||||
FROM
|
FROM
|
||||||
"sessions" "SessionEntity"
|
"user_token" "UserTokenEntity"
|
||||||
WHERE
|
WHERE
|
||||||
(
|
(
|
||||||
("SessionEntity"."userId" = $1)
|
("UserTokenEntity"."userId" = $1)
|
||||||
AND ("SessionEntity"."id" IN ($2))
|
AND ("UserTokenEntity"."id" IN ($2))
|
||||||
)
|
)
|
||||||
|
|
||||||
-- AccessRepository.library.checkOwnerAccess
|
-- AccessRepository.library.checkOwnerAccess
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
-- 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
|
|
||||||
@@ -159,12 +159,10 @@ SET
|
|||||||
COALESCE(SUM(exif."fileSizeInByte"), 0)
|
COALESCE(SUM(exif."fileSizeInByte"), 0)
|
||||||
FROM
|
FROM
|
||||||
"assets" "assets"
|
"assets" "assets"
|
||||||
LEFT JOIN "libraries" "library" ON "library"."id" = "assets"."libraryId"
|
|
||||||
AND ("library"."deletedAt" IS NULL)
|
|
||||||
LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id"
|
LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id"
|
||||||
WHERE
|
WHERE
|
||||||
"assets"."ownerId" = users.id
|
"assets"."ownerId" = users.id
|
||||||
AND "library"."type" = 'UPLOAD'
|
AND NOT "assets"."isExternal"
|
||||||
),
|
),
|
||||||
"updatedAt" = CURRENT_TIMESTAMP
|
"updatedAt" = CURRENT_TIMESTAMP
|
||||||
WHERE
|
WHERE
|
||||||
|
|||||||
48
server/src/queries/user.token.repository.sql
Normal file
48
server/src/queries/user.token.repository.sql
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
-- 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
|
||||||
@@ -9,8 +9,8 @@ import { LibraryEntity } from 'src/entities/library.entity';
|
|||||||
import { MemoryEntity } from 'src/entities/memory.entity';
|
import { MemoryEntity } from 'src/entities/memory.entity';
|
||||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { SessionEntity } from 'src/entities/session.entity';
|
|
||||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||||
|
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Brackets, In, Repository } from 'typeorm';
|
import { Brackets, In, Repository } from 'typeorm';
|
||||||
@@ -286,7 +286,7 @@ class AssetAccess implements IAssetAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AuthDeviceAccess implements IAuthDeviceAccess {
|
class AuthDeviceAccess implements IAuthDeviceAccess {
|
||||||
constructor(private sessionRepository: Repository<SessionEntity>) {}
|
constructor(private tokenRepository: Repository<UserTokenEntity>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||||
@ChunkedSet({ paramIndex: 1 })
|
@ChunkedSet({ paramIndex: 1 })
|
||||||
@@ -295,7 +295,7 @@ class AuthDeviceAccess implements IAuthDeviceAccess {
|
|||||||
return new Set();
|
return new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sessionRepository
|
return this.tokenRepository
|
||||||
.find({
|
.find({
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
where: {
|
where: {
|
||||||
@@ -457,12 +457,12 @@ export class AccessRepository implements IAccessRepository {
|
|||||||
@InjectRepository(PersonEntity) personRepository: Repository<PersonEntity>,
|
@InjectRepository(PersonEntity) personRepository: Repository<PersonEntity>,
|
||||||
@InjectRepository(AssetFaceEntity) assetFaceRepository: Repository<AssetFaceEntity>,
|
@InjectRepository(AssetFaceEntity) assetFaceRepository: Repository<AssetFaceEntity>,
|
||||||
@InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository<SharedLinkEntity>,
|
@InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository<SharedLinkEntity>,
|
||||||
@InjectRepository(SessionEntity) sessionRepository: Repository<SessionEntity>,
|
@InjectRepository(UserTokenEntity) tokenRepository: Repository<UserTokenEntity>,
|
||||||
) {
|
) {
|
||||||
this.activity = new ActivityAccess(activityRepository, albumRepository);
|
this.activity = new ActivityAccess(activityRepository, albumRepository);
|
||||||
this.album = new AlbumAccess(albumRepository, sharedLinkRepository);
|
this.album = new AlbumAccess(albumRepository, sharedLinkRepository);
|
||||||
this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository);
|
this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository);
|
||||||
this.authDevice = new AuthDeviceAccess(sessionRepository);
|
this.authDevice = new AuthDeviceAccess(tokenRepository);
|
||||||
this.library = new LibraryAccess(libraryRepository);
|
this.library = new LibraryAccess(libraryRepository);
|
||||||
this.memory = new MemoryAccess(memoryRepository);
|
this.memory = new MemoryAccess(memoryRepository);
|
||||||
this.person = new PersonAccess(assetFaceRepository, personRepository);
|
this.person = new PersonAccess(assetFaceRepository, personRepository);
|
||||||
|
|||||||
@@ -20,14 +20,15 @@ import { IMetricRepository } from 'src/interfaces/metric.interface';
|
|||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
|
import { IPluginRepository } from 'src/interfaces/plugin.interface';
|
||||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||||
|
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
@@ -51,14 +52,15 @@ import { MetricRepository } from 'src/repositories/metric.repository';
|
|||||||
import { MoveRepository } from 'src/repositories/move.repository';
|
import { MoveRepository } from 'src/repositories/move.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
|
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||||
import { SessionRepository } from 'src/repositories/session.repository';
|
|
||||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { SystemConfigRepository } from 'src/repositories/system-config.repository';
|
import { SystemConfigRepository } from 'src/repositories/system-config.repository';
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
import { TagRepository } from 'src/repositories/tag.repository';
|
import { TagRepository } from 'src/repositories/tag.repository';
|
||||||
|
import { UserTokenRepository } from 'src/repositories/user-token.repository';
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
|
|
||||||
export const repositories = [
|
export const repositories = [
|
||||||
@@ -83,14 +85,15 @@ export const repositories = [
|
|||||||
{ provide: IMoveRepository, useClass: MoveRepository },
|
{ provide: IMoveRepository, useClass: MoveRepository },
|
||||||
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
||||||
{ provide: IPersonRepository, useClass: PersonRepository },
|
{ provide: IPersonRepository, useClass: PersonRepository },
|
||||||
|
{ provide: IPluginRepository, useClass: PluginRepository },
|
||||||
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||||
{ provide: ISearchRepository, useClass: SearchRepository },
|
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||||
{ provide: ISessionRepository, useClass: SessionRepository },
|
|
||||||
{ provide: IStorageRepository, useClass: StorageRepository },
|
{ provide: IStorageRepository, useClass: StorageRepository },
|
||||||
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||||
{ provide: ITagRepository, useClass: TagRepository },
|
{ provide: ITagRepository, useClass: TagRepository },
|
||||||
{ provide: IMediaRepository, useClass: MediaRepository },
|
{ provide: IMediaRepository, useClass: MediaRepository },
|
||||||
{ provide: IUserRepository, useClass: UserRepository },
|
{ provide: IUserRepository, useClass: UserRepository },
|
||||||
|
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
|
|||||||
[JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY,
|
[JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY,
|
||||||
[JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY,
|
[JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY,
|
||||||
[JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY,
|
[JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY,
|
||||||
|
|
||||||
|
// workflows
|
||||||
|
[JobName.WORKFLOW_TRIGGER]: QueueName.BACKGROUND_TASK,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { exiftool } from 'exiftool-vendored';
|
|
||||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
@@ -10,7 +9,6 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import {
|
import {
|
||||||
CropOptions,
|
CropOptions,
|
||||||
IMediaRepository,
|
IMediaRepository,
|
||||||
ImageDimensions,
|
|
||||||
ResizeOptions,
|
ResizeOptions,
|
||||||
TranscodeOptions,
|
TranscodeOptions,
|
||||||
VideoInfo,
|
VideoInfo,
|
||||||
@@ -28,23 +26,6 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
|
constructor(@Inject(ILoggerRepository) private logger: ILoggerRepository) {
|
||||||
this.logger.setContext(MediaRepository.name);
|
this.logger.setContext(MediaRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async extract(input: string, output: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await exiftool.extractJpgFromRaw(input, output);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.debug('Could not extract JPEG from image, trying preview', error.message);
|
|
||||||
try {
|
|
||||||
await exiftool.extractPreview(input, output);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.debug('Could not extract preview from image', error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
crop(input: string | Buffer, options: CropOptions): Promise<Buffer> {
|
crop(input: string | Buffer, options: CropOptions): Promise<Buffer> {
|
||||||
return sharp(input, { failOn: 'none' })
|
return sharp(input, { failOn: 'none' })
|
||||||
.pipelineColorspace('rgb16')
|
.pipelineColorspace('rgb16')
|
||||||
@@ -152,11 +133,6 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
return Buffer.from(thumbhash.rgbaToThumbHash(info.width, info.height, data));
|
return Buffer.from(thumbhash.rgbaToThumbHash(info.width, info.height, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImageDimensions(input: string): Promise<ImageDimensions> {
|
|
||||||
const { width = 0, height = 0 } = await sharp(input).metadata();
|
|
||||||
return { width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
|
private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
|
||||||
return ffmpeg(input, { niceness: 10 })
|
return ffmpeg(input, { niceness: 10 })
|
||||||
.inputOptions(options.inputOptions)
|
.inputOptions(options.inputOptions)
|
||||||
@@ -164,4 +140,9 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
.output(output)
|
.output(output)
|
||||||
.on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
|
.on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private chainPath(existing: string, path: string) {
|
||||||
|
const separator = existing.endsWith(':') ? '' : ':';
|
||||||
|
return `${existing}${separator}${path}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
server/src/repositories/plugin.repository.ts
Normal file
56
server/src/repositories/plugin.repository.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { writeFile } from 'node:fs/promises';
|
||||||
|
import { PluginEntity } from 'src/entities/plugin.entity';
|
||||||
|
import { IPluginRepository, PluginLike, PluginSearchOptions } from 'src/interfaces/plugin.interface';
|
||||||
|
import { ImmichLogger } from 'src/utils/logger';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PluginRepository implements IPluginRepository {
|
||||||
|
private logger = new ImmichLogger(PluginRepository.name);
|
||||||
|
constructor(@InjectRepository(PluginEntity) private repository: Repository<PluginEntity>) {}
|
||||||
|
|
||||||
|
search(options: PluginSearchOptions): Promise<PluginEntity[]> {
|
||||||
|
return this.repository.find({
|
||||||
|
where: {
|
||||||
|
id: options.id,
|
||||||
|
packageId: options.namespace,
|
||||||
|
version: options.version,
|
||||||
|
name: options.name,
|
||||||
|
isEnabled: options.isEnabled,
|
||||||
|
isInstalled: options.isInstalled,
|
||||||
|
isTrusted: options.isOfficial,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
create(dto: Partial<PluginEntity>): Promise<PluginEntity> {
|
||||||
|
return this.repository.save(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: string): Promise<PluginEntity | null> {
|
||||||
|
return this.repository.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dto: Partial<PluginEntity>): Promise<PluginEntity> {
|
||||||
|
return this.repository.save(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await this.repository.delete({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(url: string, downloadPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { json } = await fetch(url);
|
||||||
|
await writeFile(downloadPath, await json());
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error downloading the plugin from ${url}. ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load(pluginPath: string): Promise<PluginLike> {
|
||||||
|
return import(pluginPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { SessionEntity } from 'src/entities/session.entity';
|
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SessionRepository implements ISessionRepository {
|
export class UserTokenRepository implements IUserTokenRepository {
|
||||||
constructor(@InjectRepository(SessionEntity) private repository: Repository<SessionEntity>) {}
|
constructor(@InjectRepository(UserTokenEntity) private repository: Repository<UserTokenEntity>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.STRING] })
|
||||||
getByToken(token: string): Promise<SessionEntity | null> {
|
getByToken(token: string): Promise<UserTokenEntity | null> {
|
||||||
return this.repository.findOne({ where: { token }, relations: { user: true } });
|
return this.repository.findOne({ where: { token }, relations: { user: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
getByUserId(userId: string): Promise<SessionEntity[]> {
|
getAll(userId: string): Promise<UserTokenEntity[]> {
|
||||||
return this.repository.find({
|
return this.repository.find({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
@@ -31,12 +31,12 @@ export class SessionRepository implements ISessionRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
create(session: Partial<SessionEntity>): Promise<SessionEntity> {
|
create(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> {
|
||||||
return this.repository.save(session);
|
return this.repository.save(userToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(session: Partial<SessionEntity>): Promise<SessionEntity> {
|
save(userToken: Partial<UserTokenEntity>): Promise<UserTokenEntity> {
|
||||||
return this.repository.save(session);
|
return this.repository.save(userToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { LibraryType } from 'src/entities/library.entity';
|
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import {
|
import {
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
@@ -118,14 +117,11 @@ export class UserRepository implements IUserRepository {
|
|||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async syncUsage(id?: string) {
|
async syncUsage(id?: string) {
|
||||||
// we can't use parameters with getQuery, hence the template string
|
|
||||||
const subQuery = this.assetRepository
|
const subQuery = this.assetRepository
|
||||||
.createQueryBuilder('assets')
|
.createQueryBuilder('assets')
|
||||||
.select('COALESCE(SUM(exif."fileSizeInByte"), 0)')
|
.select('COALESCE(SUM(exif."fileSizeInByte"), 0)')
|
||||||
.leftJoin('assets.library', 'library')
|
|
||||||
.leftJoin('assets.exifInfo', 'exif')
|
.leftJoin('assets.exifInfo', 'exif')
|
||||||
.where('assets.ownerId = users.id')
|
.where('assets.ownerId = users.id AND NOT assets.isExternal')
|
||||||
.andWhere(`library.type = '${LibraryType.UPLOAD}'`)
|
|
||||||
.withDeleted();
|
.withDeleted();
|
||||||
|
|
||||||
const query = this.userRepository
|
const query = this.userRepository
|
||||||
|
|||||||
@@ -392,9 +392,7 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.assetRepository.remove(asset);
|
await this.assetRepository.remove(asset);
|
||||||
if (asset.library.type === LibraryType.UPLOAD) {
|
await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0));
|
||||||
await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0));
|
|
||||||
}
|
|
||||||
this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, asset.ownerId, id);
|
this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, asset.ownerId, id);
|
||||||
|
|
||||||
// TODO refactor this to use cascades
|
// TODO refactor this to use cascades
|
||||||
|
|||||||
@@ -9,25 +9,25 @@ import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
|||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
|
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||||
import { authStub, loginResponseStub } from 'test/fixtures/auth.stub';
|
import { authStub, loginResponseStub } from 'test/fixtures/auth.stub';
|
||||||
import { sessionStub } from 'test/fixtures/session.stub';
|
|
||||||
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||||
|
import { userTokenStub } from 'test/fixtures/user-token.stub';
|
||||||
import { userStub } from 'test/fixtures/user.stub';
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
|
||||||
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
||||||
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
||||||
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
||||||
import { newSessionRepositoryMock } from 'test/repositories/session.repository.mock';
|
|
||||||
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
import { newSharedLinkRepositoryMock } from 'test/repositories/shared-link.repository.mock';
|
||||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||||
|
import { newUserTokenRepositoryMock } from 'test/repositories/user-token.repository.mock';
|
||||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||||
import { Mock, Mocked, vitest } from 'vitest';
|
import { Mock, Mocked, vitest } from 'vitest';
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ describe('AuthService', () => {
|
|||||||
let libraryMock: Mocked<ILibraryRepository>;
|
let libraryMock: Mocked<ILibraryRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
let configMock: Mocked<ISystemConfigRepository>;
|
let configMock: Mocked<ISystemConfigRepository>;
|
||||||
let sessionMock: Mocked<ISessionRepository>;
|
let userTokenMock: Mocked<IUserTokenRepository>;
|
||||||
let shareMock: Mocked<ISharedLinkRepository>;
|
let shareMock: Mocked<ISharedLinkRepository>;
|
||||||
let keyMock: Mocked<IKeyRepository>;
|
let keyMock: Mocked<IKeyRepository>;
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ describe('AuthService', () => {
|
|||||||
libraryMock = newLibraryRepositoryMock();
|
libraryMock = newLibraryRepositoryMock();
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
configMock = newSystemConfigRepositoryMock();
|
configMock = newSystemConfigRepositoryMock();
|
||||||
sessionMock = newSessionRepositoryMock();
|
userTokenMock = newUserTokenRepositoryMock();
|
||||||
shareMock = newSharedLinkRepositoryMock();
|
shareMock = newSharedLinkRepositoryMock();
|
||||||
keyMock = newKeyRepositoryMock();
|
keyMock = newKeyRepositoryMock();
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ describe('AuthService', () => {
|
|||||||
libraryMock,
|
libraryMock,
|
||||||
loggerMock,
|
loggerMock,
|
||||||
userMock,
|
userMock,
|
||||||
sessionMock,
|
userTokenMock,
|
||||||
shareMock,
|
shareMock,
|
||||||
keyMock,
|
keyMock,
|
||||||
);
|
);
|
||||||
@@ -139,10 +139,24 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
it('should successfully log the user in', async () => {
|
it('should successfully log the user in', async () => {
|
||||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||||
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
|
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
|
||||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate the cookie headers (insecure)', async () => {
|
||||||
|
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||||
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||||
|
await expect(
|
||||||
|
sut.login(fixtures.login, {
|
||||||
|
clientIp: '127.0.0.1',
|
||||||
|
isSecure: false,
|
||||||
|
deviceOS: '',
|
||||||
|
deviceType: '',
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(loginResponseStub.user1insecure);
|
||||||
|
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('changePassword', () => {
|
describe('changePassword', () => {
|
||||||
@@ -217,14 +231,14 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete the access token', async () => {
|
it('should delete the access token', async () => {
|
||||||
const auth = { user: { id: '123' }, session: { id: 'token123' } } as AuthDto;
|
const auth = { user: { id: '123' }, userToken: { id: 'token123' } } as AuthDto;
|
||||||
|
|
||||||
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
|
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
|
||||||
successful: true,
|
successful: true,
|
||||||
redirectUri: '/auth/login?autoLaunch=0',
|
redirectUri: '/auth/login?autoLaunch=0',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(sessionMock.delete).toHaveBeenCalledWith('token123');
|
expect(userTokenMock.delete).toHaveBeenCalledWith('token123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
|
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
|
||||||
@@ -268,11 +282,11 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
it('should validate using authorization header', async () => {
|
it('should validate using authorization header', async () => {
|
||||||
userMock.get.mockResolvedValue(userStub.user1);
|
userMock.get.mockResolvedValue(userStub.user1);
|
||||||
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
|
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
|
||||||
const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
|
const client = { request: { headers: { authorization: 'Bearer auth_token' } } };
|
||||||
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({
|
await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({
|
||||||
user: userStub.user1,
|
user: userStub.user1,
|
||||||
session: sessionStub.valid,
|
userToken: userTokenStub.userToken,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -322,29 +336,37 @@ describe('AuthService', () => {
|
|||||||
|
|
||||||
describe('validate - user token', () => {
|
describe('validate - user token', () => {
|
||||||
it('should throw if no token is found', async () => {
|
it('should throw if no token is found', async () => {
|
||||||
sessionMock.getByToken.mockResolvedValue(null);
|
userTokenMock.getByToken.mockResolvedValue(null);
|
||||||
const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' };
|
const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' };
|
||||||
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an auth dto', async () => {
|
it('should return an auth dto', async () => {
|
||||||
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
|
userTokenMock.getByToken.mockResolvedValue(userTokenStub.userToken);
|
||||||
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
||||||
await expect(sut.validate(headers, {})).resolves.toEqual({
|
await expect(sut.validate(headers, {})).resolves.toEqual({
|
||||||
user: userStub.user1,
|
user: userStub.user1,
|
||||||
session: sessionStub.valid,
|
userToken: userTokenStub.userToken,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update when access time exceeds an hour', async () => {
|
it('should update when access time exceeds an hour', async () => {
|
||||||
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive);
|
userTokenMock.getByToken.mockResolvedValue(userTokenStub.inactiveToken);
|
||||||
sessionMock.update.mockResolvedValue(sessionStub.valid);
|
userTokenMock.save.mockResolvedValue(userTokenStub.userToken);
|
||||||
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' };
|
||||||
await expect(sut.validate(headers, {})).resolves.toEqual({
|
await expect(sut.validate(headers, {})).resolves.toEqual({
|
||||||
user: userStub.user1,
|
user: userStub.user1,
|
||||||
session: sessionStub.valid,
|
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',
|
||||||
});
|
});
|
||||||
expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -364,6 +386,55 @@ describe('AuthService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getDevices', () => {
|
||||||
|
it('should get the devices', async () => {
|
||||||
|
userTokenMock.getAll.mockResolvedValue([userTokenStub.userToken, userTokenStub.inactiveToken]);
|
||||||
|
await expect(sut.getDevices(authStub.user1)).resolves.toEqual([
|
||||||
|
{
|
||||||
|
createdAt: '2021-01-01T00:00:00.000Z',
|
||||||
|
current: true,
|
||||||
|
deviceOS: '',
|
||||||
|
deviceType: '',
|
||||||
|
id: 'token-id',
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: '2021-01-01T00:00:00.000Z',
|
||||||
|
current: false,
|
||||||
|
deviceOS: 'Android',
|
||||||
|
deviceType: 'Mobile',
|
||||||
|
id: 'not_active',
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logoutDevices', () => {
|
||||||
|
it('should logout all devices', async () => {
|
||||||
|
userTokenMock.getAll.mockResolvedValue([userTokenStub.inactiveToken, userTokenStub.userToken]);
|
||||||
|
|
||||||
|
await sut.logoutDevices(authStub.user1);
|
||||||
|
|
||||||
|
expect(userTokenMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||||
|
expect(userTokenMock.delete).toHaveBeenCalledWith('not_active');
|
||||||
|
expect(userTokenMock.delete).not.toHaveBeenCalledWith('token-id');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logoutDevice', () => {
|
||||||
|
it('should logout the device', async () => {
|
||||||
|
accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1']));
|
||||||
|
|
||||||
|
await sut.logoutDevice(authStub.user1, 'token-1');
|
||||||
|
|
||||||
|
expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.user.id, new Set(['token-1']));
|
||||||
|
expect(userTokenMock.delete).toHaveBeenCalledWith('token-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getMobileRedirect', () => {
|
describe('getMobileRedirect', () => {
|
||||||
it('should pass along the query params', () => {
|
it('should pass along the query params', () => {
|
||||||
expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual('app.immich:/?code=123&state=456');
|
expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual('app.immich:/?code=123&state=456');
|
||||||
@@ -392,7 +463,7 @@ describe('AuthService', () => {
|
|||||||
configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
|
configMock.load.mockResolvedValue(systemConfigStub.noAutoRegister);
|
||||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||||
userMock.update.mockResolvedValue(userStub.user1);
|
userMock.update.mockResolvedValue(userStub.user1);
|
||||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
loginResponseStub.user1oauth,
|
||||||
@@ -407,7 +478,7 @@ describe('AuthService', () => {
|
|||||||
userMock.getByEmail.mockResolvedValue(null);
|
userMock.getByEmail.mockResolvedValue(null);
|
||||||
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
userMock.getAdmin.mockResolvedValue(userStub.user1);
|
||||||
userMock.create.mockResolvedValue(userStub.user1);
|
userMock.create.mockResolvedValue(userStub.user1);
|
||||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||||
|
|
||||||
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
|
||||||
loginResponseStub.user1oauth,
|
loginResponseStub.user1oauth,
|
||||||
@@ -420,7 +491,7 @@ describe('AuthService', () => {
|
|||||||
it('should use the mobile redirect override', async () => {
|
it('should use the mobile redirect override', async () => {
|
||||||
configMock.load.mockResolvedValue(systemConfigStub.override);
|
configMock.load.mockResolvedValue(systemConfigStub.override);
|
||||||
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
||||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||||
|
|
||||||
await sut.callback({ url: `app.immich:/?code=abc123` }, loginDetails);
|
await sut.callback({ url: `app.immich:/?code=abc123` }, loginDetails);
|
||||||
|
|
||||||
@@ -430,7 +501,7 @@ describe('AuthService', () => {
|
|||||||
it('should use the mobile redirect override for ios urls with multiple slashes', async () => {
|
it('should use the mobile redirect override for ios urls with multiple slashes', async () => {
|
||||||
configMock.load.mockResolvedValue(systemConfigStub.override);
|
configMock.load.mockResolvedValue(systemConfigStub.override);
|
||||||
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
userMock.getByOAuthId.mockResolvedValue(userStub.user1);
|
||||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
userTokenMock.create.mockResolvedValue(userTokenStub.userToken);
|
||||||
|
|
||||||
await sut.callback({ url: `app.immich:///?code=abc123` }, loginDetails);
|
await sut.callback({ url: `app.immich:///?code=abc123` }, loginDetails);
|
||||||
|
|
||||||
|
|||||||
@@ -10,22 +10,31 @@ import cookieParser from 'cookie';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { IncomingHttpHeaders } from 'node:http';
|
import { IncomingHttpHeaders } from 'node:http';
|
||||||
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
||||||
import { AuthType, LOGIN_URL, MOBILE_REDIRECT } from 'src/constants';
|
import {
|
||||||
import { AccessCore } from 'src/cores/access.core';
|
AuthType,
|
||||||
|
IMMICH_ACCESS_COOKIE,
|
||||||
|
IMMICH_API_KEY_HEADER,
|
||||||
|
IMMICH_AUTH_TYPE_COOKIE,
|
||||||
|
IMMICH_IS_AUTHENTICATED,
|
||||||
|
LOGIN_URL,
|
||||||
|
MOBILE_REDIRECT,
|
||||||
|
} from 'src/constants';
|
||||||
|
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { UserCore } from 'src/cores/user.core';
|
import { UserCore } from 'src/cores/user.core';
|
||||||
import {
|
import {
|
||||||
|
AuthDeviceResponseDto,
|
||||||
AuthDto,
|
AuthDto,
|
||||||
ChangePasswordDto,
|
ChangePasswordDto,
|
||||||
ImmichCookie,
|
|
||||||
ImmichHeader,
|
|
||||||
LoginCredentialDto,
|
LoginCredentialDto,
|
||||||
|
LoginResponseDto,
|
||||||
LogoutResponseDto,
|
LogoutResponseDto,
|
||||||
OAuthAuthorizeResponseDto,
|
OAuthAuthorizeResponseDto,
|
||||||
OAuthCallbackDto,
|
OAuthCallbackDto,
|
||||||
OAuthConfigDto,
|
OAuthConfigDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
mapLoginResponse,
|
mapLoginResponse,
|
||||||
|
mapUserToken,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||||
import { SystemConfig } from 'src/entities/system-config.entity';
|
import { SystemConfig } from 'src/entities/system-config.entity';
|
||||||
@@ -35,9 +44,9 @@ import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
|||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ISessionRepository } from 'src/interfaces/session.interface';
|
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
|
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { HumanReadableSize } from 'src/utils/bytes';
|
import { HumanReadableSize } from 'src/utils/bytes';
|
||||||
|
|
||||||
@@ -48,6 +57,11 @@ export interface LoginDetails {
|
|||||||
deviceOS: string;
|
deviceOS: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LoginResponse {
|
||||||
|
response: LoginResponseDto;
|
||||||
|
cookie: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface OAuthProfile extends UserinfoResponse {
|
interface OAuthProfile extends UserinfoResponse {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
@@ -71,7 +85,7 @@ export class AuthService {
|
|||||||
@Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
|
@Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@Inject(ISessionRepository) private sessionRepository: ISessionRepository,
|
@Inject(IUserTokenRepository) private userTokenRepository: IUserTokenRepository,
|
||||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
||||||
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
@Inject(IKeyRepository) private keyRepository: IKeyRepository,
|
||||||
) {
|
) {
|
||||||
@@ -83,7 +97,7 @@ export class AuthService {
|
|||||||
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(dto: LoginCredentialDto, details: LoginDetails) {
|
async login(dto: LoginCredentialDto, details: LoginDetails): Promise<LoginResponse> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig();
|
||||||
if (!config.passwordLogin.enabled) {
|
if (!config.passwordLogin.enabled) {
|
||||||
throw new UnauthorizedException('Password login has been disabled');
|
throw new UnauthorizedException('Password login has been disabled');
|
||||||
@@ -102,12 +116,12 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException('Incorrect email or password');
|
throw new UnauthorizedException('Incorrect email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.createLoginResponse(user, details);
|
return this.createLoginResponse(user, AuthType.PASSWORD, details);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
||||||
if (auth.session) {
|
if (auth.userToken) {
|
||||||
await this.sessionRepository.delete(auth.session.id);
|
await this.userTokenRepository.delete(auth.userToken.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -149,20 +163,19 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
|
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
|
||||||
const shareKey = (headers[ImmichHeader.SHARED_LINK_TOKEN] || params.key) as string;
|
const shareKey = (headers['x-immich-share-key'] || params.key) as string;
|
||||||
const session = (headers[ImmichHeader.USER_TOKEN] ||
|
const userToken = (headers['x-immich-user-token'] ||
|
||||||
headers[ImmichHeader.SESSION_TOKEN] ||
|
params.userToken ||
|
||||||
params.sessionKey ||
|
|
||||||
this.getBearerToken(headers) ||
|
this.getBearerToken(headers) ||
|
||||||
this.getCookieToken(headers)) as string;
|
this.getCookieToken(headers)) as string;
|
||||||
const apiKey = (headers[ImmichHeader.API_KEY] || params.apiKey) as string;
|
const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string;
|
||||||
|
|
||||||
if (shareKey) {
|
if (shareKey) {
|
||||||
return this.validateSharedLink(shareKey);
|
return this.validateSharedLink(shareKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session) {
|
if (userToken) {
|
||||||
return this.validateSession(session);
|
return this.validateUserToken(userToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
@@ -172,6 +185,26 @@ export class AuthService {
|
|||||||
throw new UnauthorizedException('Authentication required');
|
throw new UnauthorizedException('Authentication required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDevices(auth: AuthDto): Promise<AuthDeviceResponseDto[]> {
|
||||||
|
const userTokens = await this.userTokenRepository.getAll(auth.user.id);
|
||||||
|
return userTokens.map((userToken) => mapUserToken(userToken, auth.userToken?.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async logoutDevice(auth: AuthDto, id: string): Promise<void> {
|
||||||
|
await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id);
|
||||||
|
await this.userTokenRepository.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async logoutDevices(auth: AuthDto): Promise<void> {
|
||||||
|
const devices = await this.userTokenRepository.getAll(auth.user.id);
|
||||||
|
for (const device of devices) {
|
||||||
|
if (device.id === auth.userToken?.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await this.userTokenRepository.delete(device.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getMobileRedirect(url: string) {
|
getMobileRedirect(url: string) {
|
||||||
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
|
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
|
||||||
}
|
}
|
||||||
@@ -192,7 +225,10 @@ export class AuthService {
|
|||||||
return { url };
|
return { url };
|
||||||
}
|
}
|
||||||
|
|
||||||
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
|
async callback(
|
||||||
|
dto: OAuthCallbackDto,
|
||||||
|
loginDetails: LoginDetails,
|
||||||
|
): Promise<{ response: LoginResponseDto; cookie: string[] }> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig();
|
||||||
const profile = await this.getOAuthProfile(config, dto.url);
|
const profile = await this.getOAuthProfile(config, dto.url);
|
||||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||||
@@ -241,7 +277,7 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.createLoginResponse(user, loginDetails);
|
return this.createLoginResponse(user, AuthType.OAUTH, loginDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
|
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
|
||||||
@@ -338,7 +374,7 @@ export class AuthService {
|
|||||||
|
|
||||||
private getCookieToken(headers: IncomingHttpHeaders): string | null {
|
private getCookieToken(headers: IncomingHttpHeaders): string | null {
|
||||||
const cookies = cookieParser.parse(headers.cookie || '');
|
const cookies = cookieParser.parse(headers.cookie || '');
|
||||||
return cookies[ImmichCookie.ACCESS_TOKEN] || null;
|
return cookies[IMMICH_ACCESS_COOKIE] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateSharedLink(key: string | string[]): Promise<AuthDto> {
|
async validateSharedLink(key: string | string[]): Promise<AuthDto> {
|
||||||
@@ -372,36 +408,57 @@ export class AuthService {
|
|||||||
return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
|
return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateSession(tokenValue: string): Promise<AuthDto> {
|
private async validateUserToken(tokenValue: string): Promise<AuthDto> {
|
||||||
const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
|
const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
|
||||||
let session = await this.sessionRepository.getByToken(hashedToken);
|
let userToken = await this.userTokenRepository.getByToken(hashedToken);
|
||||||
|
|
||||||
if (session?.user) {
|
if (userToken?.user) {
|
||||||
const now = DateTime.now();
|
const now = DateTime.now();
|
||||||
const updatedAt = DateTime.fromJSDate(session.updatedAt);
|
const updatedAt = DateTime.fromJSDate(userToken.updatedAt);
|
||||||
const diff = now.diff(updatedAt, ['hours']);
|
const diff = now.diff(updatedAt, ['hours']);
|
||||||
if (diff.hours > 1) {
|
if (diff.hours > 1) {
|
||||||
session = await this.sessionRepository.update({ id: session.id, updatedAt: new Date() });
|
userToken = await this.userTokenRepository.save({ ...userToken, updatedAt: new Date() });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user: session.user, session: session };
|
return { user: userToken.user, userToken };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnauthorizedException('Invalid user token');
|
throw new UnauthorizedException('Invalid user token');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createLoginResponse(user: UserEntity, loginDetails: LoginDetails) {
|
private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) {
|
||||||
const key = this.cryptoRepository.newPassword(32);
|
const key = this.cryptoRepository.newPassword(32);
|
||||||
const token = this.cryptoRepository.hashSha256(key);
|
const token = this.cryptoRepository.hashSha256(key);
|
||||||
|
|
||||||
await this.sessionRepository.create({
|
await this.userTokenRepository.create({
|
||||||
token,
|
token,
|
||||||
user,
|
user,
|
||||||
deviceOS: loginDetails.deviceOS,
|
deviceOS: loginDetails.deviceOS,
|
||||||
deviceType: loginDetails.deviceType,
|
deviceType: loginDetails.deviceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapLoginResponse(user, key);
|
const response = mapLoginResponse(user, key);
|
||||||
|
const cookie = this.getCookies(response, authType, loginDetails);
|
||||||
|
return { response, cookie };
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCookies(loginResponse: LoginResponseDto, authType: AuthType, { isSecure }: LoginDetails) {
|
||||||
|
const maxAge = 400 * 24 * 3600; // 400 days
|
||||||
|
|
||||||
|
let authTypeCookie = '';
|
||||||
|
let accessTokenCookie = '';
|
||||||
|
let isAuthenticatedCookie = '';
|
||||||
|
|
||||||
|
if (isSecure) {
|
||||||
|
accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||||
|
authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||||
|
isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||||
|
} else {
|
||||||
|
accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||||
|
authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||||
|
isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||||
|
}
|
||||||
|
return [accessTokenCookie, authTypeCookie, isAuthenticatedCookie];
|
||||||
}
|
}
|
||||||
|
|
||||||
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import { MetadataService } from 'src/services/metadata.service';
|
|||||||
import { MicroservicesService } from 'src/services/microservices.service';
|
import { MicroservicesService } from 'src/services/microservices.service';
|
||||||
import { PartnerService } from 'src/services/partner.service';
|
import { PartnerService } from 'src/services/partner.service';
|
||||||
import { PersonService } from 'src/services/person.service';
|
import { PersonService } from 'src/services/person.service';
|
||||||
|
import { PluginService } from 'src/services/plugin.service';
|
||||||
import { SearchService } from 'src/services/search.service';
|
import { SearchService } from 'src/services/search.service';
|
||||||
import { ServerInfoService } from 'src/services/server-info.service';
|
import { ServerInfoService } from 'src/services/server-info.service';
|
||||||
import { SessionService } from 'src/services/session.service';
|
|
||||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
import { SmartInfoService } from 'src/services/smart-info.service';
|
import { SmartInfoService } from 'src/services/smart-info.service';
|
||||||
import { StorageTemplateService } from 'src/services/storage-template.service';
|
import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||||
@@ -29,6 +29,7 @@ import { TagService } from 'src/services/tag.service';
|
|||||||
import { TimelineService } from 'src/services/timeline.service';
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
import { TrashService } from 'src/services/trash.service';
|
import { TrashService } from 'src/services/trash.service';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
|
import { WorkflowService } from 'src/services/workflow.service';
|
||||||
|
|
||||||
export const services = [
|
export const services = [
|
||||||
ApiService,
|
ApiService,
|
||||||
@@ -49,9 +50,9 @@ export const services = [
|
|||||||
MetadataService,
|
MetadataService,
|
||||||
PartnerService,
|
PartnerService,
|
||||||
PersonService,
|
PersonService,
|
||||||
|
PluginService,
|
||||||
SearchService,
|
SearchService,
|
||||||
ServerInfoService,
|
ServerInfoService,
|
||||||
SessionService,
|
|
||||||
SharedLinkService,
|
SharedLinkService,
|
||||||
SmartInfoService,
|
SmartInfoService,
|
||||||
StorageService,
|
StorageService,
|
||||||
@@ -62,4 +63,5 @@ export const services = [
|
|||||||
TimelineService,
|
TimelineService,
|
||||||
TrashService,
|
TrashService,
|
||||||
UserService,
|
UserService,
|
||||||
|
WorkflowService,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
JobStatus,
|
JobStatus,
|
||||||
QueueCleanType,
|
QueueCleanType,
|
||||||
QueueName,
|
QueueName,
|
||||||
|
WorkflowTriggerType,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
||||||
@@ -294,6 +295,13 @@ export class JobService {
|
|||||||
if (asset && asset.isVisible) {
|
if (asset && asset.isVisible) {
|
||||||
this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset));
|
this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (asset) {
|
||||||
|
await this.jobRepository.queue({
|
||||||
|
name: JobName.WORKFLOW_TRIGGER,
|
||||||
|
data: { type: WorkflowTriggerType.ASSET_UPLOAD, data: { assetId: asset.id } },
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -225,15 +225,6 @@ describe(MediaService.name, () => {
|
|||||||
expect(assetMock.update).not.toHaveBeenCalledWith();
|
expect(assetMock.update).not.toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip invisible assets', async () => {
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
|
||||||
|
|
||||||
expect(await sut.handleGeneratePreview({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
|
|
||||||
|
|
||||||
expect(mediaMock.resize).not.toHaveBeenCalled();
|
|
||||||
expect(assetMock.update).not.toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
|
it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]);
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
@@ -362,15 +353,6 @@ describe(MediaService.name, () => {
|
|||||||
expect(assetMock.update).not.toHaveBeenCalledWith();
|
expect(assetMock.update).not.toHaveBeenCalledWith();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip invisible assets', async () => {
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
|
||||||
|
|
||||||
expect(await sut.handleGenerateThumbnail({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
|
|
||||||
|
|
||||||
expect(mediaMock.resize).not.toHaveBeenCalled();
|
|
||||||
expect(assetMock.update).not.toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(Object.values(ImageFormat))(
|
it.each(Object.values(ImageFormat))(
|
||||||
'should generate a %s thumbnail for an image when specified',
|
'should generate a %s thumbnail for an image when specified',
|
||||||
async (format) => {
|
async (format) => {
|
||||||
@@ -393,12 +375,14 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should generate a P3 thumbnail for a wide gamut image', async () => {
|
it('should generate a P3 thumbnail for a wide gamut image', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
assetMock.getByIds.mockResolvedValue([
|
||||||
|
{ ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity },
|
||||||
|
]);
|
||||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
||||||
|
|
||||||
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
|
expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
|
||||||
expect(mediaMock.resize).toHaveBeenCalledWith(
|
expect(mediaMock.resize).toHaveBeenCalledWith(
|
||||||
assetStub.imageDng.originalPath,
|
'/original/path.jpg',
|
||||||
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
||||||
{
|
{
|
||||||
format: ImageFormat.WEBP,
|
format: ImageFormat.WEBP,
|
||||||
@@ -413,96 +397,7 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract embedded image if enabled and available', async () => {
|
describe('handleGenerateThumbhashThumbnail', () => {
|
||||||
mediaMock.extract.mockResolvedValue(true);
|
|
||||||
mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]);
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
|
||||||
|
|
||||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
|
||||||
|
|
||||||
const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString();
|
|
||||||
expect(mediaMock.resize.mock.calls).toEqual([
|
|
||||||
[
|
|
||||||
extractedPath,
|
|
||||||
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
|
||||||
{
|
|
||||||
format: ImageFormat.WEBP,
|
|
||||||
size: 250,
|
|
||||||
quality: 80,
|
|
||||||
colorspace: Colorspace.P3,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
expect(extractedPath?.endsWith('.tmp')).toBe(true);
|
|
||||||
expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resize original image if embedded image is too small', async () => {
|
|
||||||
mediaMock.extract.mockResolvedValue(true);
|
|
||||||
mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
|
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]);
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
|
||||||
|
|
||||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
|
||||||
|
|
||||||
expect(mediaMock.resize.mock.calls).toEqual([
|
|
||||||
[
|
|
||||||
assetStub.imageDng.originalPath,
|
|
||||||
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
|
||||||
{
|
|
||||||
format: ImageFormat.WEBP,
|
|
||||||
size: 250,
|
|
||||||
quality: 80,
|
|
||||||
colorspace: Colorspace.P3,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString();
|
|
||||||
expect(extractedPath?.endsWith('.tmp')).toBe(true);
|
|
||||||
expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resize original image if embedded image not found', async () => {
|
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: true }]);
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
|
||||||
|
|
||||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
|
||||||
|
|
||||||
expect(mediaMock.resize).toHaveBeenCalledWith(
|
|
||||||
assetStub.imageDng.originalPath,
|
|
||||||
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
|
||||||
{
|
|
||||||
format: ImageFormat.WEBP,
|
|
||||||
size: 250,
|
|
||||||
quality: 80,
|
|
||||||
colorspace: Colorspace.P3,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resize original image if embedded image extraction is not enabled', async () => {
|
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_EXTRACT_EMBEDDED, value: false }]);
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
|
|
||||||
|
|
||||||
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
|
|
||||||
|
|
||||||
expect(mediaMock.extract).not.toHaveBeenCalled();
|
|
||||||
expect(mediaMock.resize).toHaveBeenCalledWith(
|
|
||||||
assetStub.imageDng.originalPath,
|
|
||||||
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
|
||||||
{
|
|
||||||
format: ImageFormat.WEBP,
|
|
||||||
size: 250,
|
|
||||||
quality: 80,
|
|
||||||
colorspace: Colorspace.P3,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleGenerateThumbhash', () => {
|
|
||||||
it('should skip thumbhash generation if asset not found', async () => {
|
it('should skip thumbhash generation if asset not found', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([]);
|
assetMock.getByIds.mockResolvedValue([]);
|
||||||
await sut.handleGenerateThumbhash({ id: assetStub.image.id });
|
await sut.handleGenerateThumbhash({ id: assetStub.image.id });
|
||||||
@@ -515,15 +410,6 @@ describe(MediaService.name, () => {
|
|||||||
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
|
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip invisible assets', async () => {
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
|
||||||
|
|
||||||
expect(await sut.handleGenerateThumbhash({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
|
|
||||||
|
|
||||||
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
|
|
||||||
expect(assetMock.update).not.toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should generate a thumbhash', async () => {
|
it('should generate a thumbhash', async () => {
|
||||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
|
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
|
||||||
import { dirname } from 'node:path';
|
|
||||||
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
@@ -43,7 +42,6 @@ import {
|
|||||||
VAAPIConfig,
|
VAAPIConfig,
|
||||||
VP9Config,
|
VP9Config,
|
||||||
} from 'src/utils/media';
|
} from 'src/utils/media';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -79,7 +77,7 @@ export class MediaService {
|
|||||||
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination, { isVisible: true })
|
? this.assetRepository.getAll(pagination)
|
||||||
: this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
|
: this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,10 +178,6 @@ export class MediaService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!asset.isVisible) {
|
|
||||||
return JobStatus.SKIPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
|
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
|
||||||
await this.assetRepository.update({ id: asset.id, previewPath });
|
await this.assetRepository.update({ id: asset.id, previewPath });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
@@ -197,21 +191,9 @@ export class MediaService {
|
|||||||
|
|
||||||
switch (asset.type) {
|
switch (asset.type) {
|
||||||
case AssetType.IMAGE: {
|
case AssetType.IMAGE: {
|
||||||
const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath);
|
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
||||||
const extractedPath = StorageCore.getTempPathInDir(dirname(path));
|
const imageOptions = { format, size, colorspace, quality: image.quality };
|
||||||
const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath));
|
await this.mediaRepository.resize(asset.originalPath, path, imageOptions);
|
||||||
|
|
||||||
try {
|
|
||||||
const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize));
|
|
||||||
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
|
||||||
const imageOptions = { format, size, colorspace, quality: image.quality };
|
|
||||||
|
|
||||||
await this.mediaRepository.resize(useExtracted ? extractedPath : asset.originalPath, path, imageOptions);
|
|
||||||
} finally {
|
|
||||||
if (didExtract) {
|
|
||||||
await this.storageRepository.unlink(extractedPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,10 +230,6 @@ export class MediaService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!asset.isVisible) {
|
|
||||||
return JobStatus.SKIPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
|
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
|
||||||
await this.assetRepository.update({ id: asset.id, thumbnailPath });
|
await this.assetRepository.update({ id: asset.id, thumbnailPath });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
@@ -259,15 +237,7 @@ export class MediaService {
|
|||||||
|
|
||||||
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
|
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset?.previewPath) {
|
||||||
return JobStatus.FAILED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!asset.isVisible) {
|
|
||||||
return JobStatus.SKIPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!asset.previewPath) {
|
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +511,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseBitrateToBps(bitrateString: string) {
|
parseBitrateToBps(bitrateString: string) {
|
||||||
const bitrateValue = Number.parseInt(bitrateString);
|
const bitrateValue = Number.parseInt(bitrateString);
|
||||||
|
|
||||||
if (Number.isNaN(bitrateValue)) {
|
if (Number.isNaN(bitrateValue)) {
|
||||||
@@ -556,11 +526,4 @@ export class MediaService {
|
|||||||
return bitrateValue;
|
return bitrateValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async shouldUseExtractedImage(extractedPath: string, targetSize: number) {
|
|
||||||
const { width, height } = await this.mediaRepository.getImageDimensions(extractedPath);
|
|
||||||
const extractedSize = Math.min(width, height);
|
|
||||||
|
|
||||||
return extractedSize >= targetSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
|
|||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
|
||||||
import { MetadataService, Orientation } from 'src/services/metadata.service';
|
import { MetadataService, Orientation } from 'src/services/metadata.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { fileStub } from 'test/fixtures/file.stub';
|
import { fileStub } from 'test/fixtures/file.stub';
|
||||||
@@ -36,7 +35,6 @@ import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
|||||||
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
||||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
describe(MetadataService.name, () => {
|
describe(MetadataService.name, () => {
|
||||||
@@ -52,7 +50,6 @@ describe(MetadataService.name, () => {
|
|||||||
let storageMock: Mocked<IStorageRepository>;
|
let storageMock: Mocked<IStorageRepository>;
|
||||||
let eventMock: Mocked<IEventRepository>;
|
let eventMock: Mocked<IEventRepository>;
|
||||||
let databaseMock: Mocked<IDatabaseRepository>;
|
let databaseMock: Mocked<IDatabaseRepository>;
|
||||||
let userMock: Mocked<IUserRepository>;
|
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let loggerMock: Mocked<ILoggerRepository>;
|
||||||
let sut: MetadataService;
|
let sut: MetadataService;
|
||||||
|
|
||||||
@@ -69,7 +66,6 @@ describe(MetadataService.name, () => {
|
|||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
mediaMock = newMediaRepositoryMock();
|
mediaMock = newMediaRepositoryMock();
|
||||||
databaseMock = newDatabaseRepositoryMock();
|
databaseMock = newDatabaseRepositoryMock();
|
||||||
userMock = newUserRepositoryMock();
|
|
||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
|
|
||||||
sut = new MetadataService(
|
sut = new MetadataService(
|
||||||
@@ -85,7 +81,6 @@ describe(MetadataService.name, () => {
|
|||||||
personMock,
|
personMock,
|
||||||
storageMock,
|
storageMock,
|
||||||
configMock,
|
configMock,
|
||||||
userMock,
|
|
||||||
loggerMock,
|
loggerMock,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -377,7 +372,6 @@ describe(MetadataService.name, () => {
|
|||||||
);
|
);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
||||||
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
@@ -406,7 +400,6 @@ describe(MetadataService.name, () => {
|
|||||||
);
|
);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
||||||
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
@@ -433,7 +426,6 @@ describe(MetadataService.name, () => {
|
|||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
||||||
expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
|
expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
|
||||||
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
@@ -452,8 +444,6 @@ describe(MetadataService.name, () => {
|
|||||||
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
||||||
assetMock.getByChecksum.mockResolvedValue(null);
|
assetMock.getByChecksum.mockResolvedValue(null);
|
||||||
assetMock.create.mockImplementation((asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }));
|
assetMock.create.mockImplementation((asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }));
|
||||||
const video = randomBytes(512);
|
|
||||||
storageMock.readFile.mockResolvedValue(video);
|
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||||
expect(jobMock.queue).toHaveBeenNthCalledWith(2, {
|
expect(jobMock.queue).toHaveBeenNthCalledWith(2, {
|
||||||
@@ -462,7 +452,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => {
|
it('should not create a new motionphoto video asset if the of the extracted video matches an existing asset', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
||||||
metadataMock.readTags.mockResolvedValue({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
@@ -472,8 +462,6 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
||||||
assetMock.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
assetMock.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||||
const video = randomBytes(512);
|
|
||||||
storageMock.readFile.mockResolvedValue(video);
|
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||||
expect(assetMock.create).toHaveBeenCalledTimes(0);
|
expect(assetMock.create).toHaveBeenCalledTimes(0);
|
||||||
@@ -507,26 +495,6 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not update storage usage if motion photo is external', async () => {
|
|
||||||
assetMock.getByIds.mockResolvedValue([
|
|
||||||
{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null, isExternal: true },
|
|
||||||
]);
|
|
||||||
metadataMock.readTags.mockResolvedValue({
|
|
||||||
Directory: 'foo/bar/',
|
|
||||||
MotionPhoto: 1,
|
|
||||||
MicroVideo: 1,
|
|
||||||
MicroVideoOffset: 1,
|
|
||||||
});
|
|
||||||
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
|
||||||
assetMock.getByChecksum.mockResolvedValue(null);
|
|
||||||
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
||||||
const video = randomBytes(512);
|
|
||||||
storageMock.readFile.mockResolvedValue(video);
|
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
|
||||||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should save all metadata', async () => {
|
it('should save all metadata', async () => {
|
||||||
const tags: ImmichTags = {
|
const tags: ImmichTags = {
|
||||||
BitsPerSample: 1,
|
BitsPerSample: 1,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { IMoveRepository } from 'src/interfaces/move.interface';
|
|||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
|
||||||
import { handlePromiseError } from 'src/utils/misc';
|
import { handlePromiseError } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@@ -115,7 +114,6 @@ export class MetadataService {
|
|||||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(MetadataService.name);
|
this.logger.setContext(MetadataService.name);
|
||||||
@@ -448,14 +446,10 @@ export class MetadataService {
|
|||||||
this.storageCore.ensureFolders(motionPath);
|
this.storageCore.ensureFolders(motionPath);
|
||||||
await this.storageRepository.writeFile(motionAsset.originalPath, video);
|
await this.storageRepository.writeFile(motionAsset.originalPath, video);
|
||||||
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } });
|
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } });
|
||||||
if (!asset.isExternal) {
|
|
||||||
await this.userRepository.updateUsage(asset.ownerId, video.byteLength);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset.livePhotoVideoId !== motionAsset.id) {
|
if (asset.livePhotoVideoId !== motionAsset.id) {
|
||||||
await this.assetRepository.update({ id: asset.id, livePhotoVideoId: motionAsset.id });
|
await this.assetRepository.update({ id: asset.id, livePhotoVideoId: motionAsset.id });
|
||||||
|
|
||||||
// If the asset already had an associated livePhotoVideo, delete it, because
|
// If the asset already had an associated livePhotoVideo, delete it, because
|
||||||
// its checksum doesn't match the checksum of the motionAsset we just extracted
|
// its checksum doesn't match the checksum of the motionAsset we just extracted
|
||||||
// (if it did, getByChecksum() would've returned a motionAsset with the same ID as livePhotoVideoId)
|
// (if it did, getByChecksum() would've returned a motionAsset with the same ID as livePhotoVideoId)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { StorageTemplateService } from 'src/services/storage-template.service';
|
|||||||
import { StorageService } from 'src/services/storage.service';
|
import { StorageService } from 'src/services/storage.service';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
|
import { WorkflowService } from 'src/services/workflow.service';
|
||||||
import { otelSDK } from 'src/utils/instrumentation';
|
import { otelSDK } from 'src/utils/instrumentation';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -31,6 +32,7 @@ export class MicroservicesService {
|
|||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private databaseService: DatabaseService,
|
private databaseService: DatabaseService,
|
||||||
|
private workflowService: WorkflowService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -77,6 +79,7 @@ export class MicroservicesService {
|
|||||||
[JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleOfflineRemoval(data),
|
[JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleOfflineRemoval(data),
|
||||||
[JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data),
|
[JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data),
|
||||||
[JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(),
|
[JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(),
|
||||||
|
[JobName.WORKFLOW_TRIGGER]: (data) => this.workflowService.handleTrigger(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.metadataService.init();
|
await this.metadataService.init();
|
||||||
|
|||||||
@@ -292,12 +292,7 @@ export class PersonService {
|
|||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination, {
|
? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true })
|
||||||
orderDirection: 'DESC',
|
|
||||||
withFaces: true,
|
|
||||||
withArchived: true,
|
|
||||||
isVisible: true,
|
|
||||||
})
|
|
||||||
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
|
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -327,10 +322,6 @@ export class PersonService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!asset.isVisible) {
|
|
||||||
return JobStatus.SKIPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
const faces = await this.machineLearningRepository.detectFaces(
|
const faces = await this.machineLearningRepository.detectFaces(
|
||||||
machineLearning.url,
|
machineLearning.url,
|
||||||
{ imagePath: asset.previewPath },
|
{ imagePath: asset.previewPath },
|
||||||
@@ -433,7 +424,7 @@ export class PersonService {
|
|||||||
|
|
||||||
this.logger.debug(`Face ${id} has ${matches.length} matches`);
|
this.logger.debug(`Face ${id} has ${matches.length} matches`);
|
||||||
|
|
||||||
const isCore = matches.length >= machineLearning.facialRecognition.minFaces && !face.asset.isArchived;
|
const isCore = matches.length >= machineLearning.facialRecognition.minFaces;
|
||||||
if (!isCore && !deferred) {
|
if (!isCore && !deferred) {
|
||||||
this.logger.debug(`Deferring non-core face ${id} for later processing`);
|
this.logger.debug(`Deferring non-core face ${id} for later processing`);
|
||||||
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });
|
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });
|
||||||
|
|||||||
108
server/src/services/plugin.service.ts
Normal file
108
server/src/services/plugin.service.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { BadRequestException, Inject } from '@nestjs/common';
|
||||||
|
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { PluginImportDto, PluginUpdateDto, SearchPluginDto, mapPlugin } from 'src/dtos/plugin.dto';
|
||||||
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { IPluginRepository, Plugin, PluginFactory } from 'src/interfaces/plugin.interface';
|
||||||
|
|
||||||
|
export class PluginService {
|
||||||
|
private access: AccessCore;
|
||||||
|
constructor(
|
||||||
|
@Inject(IPluginRepository) private pluginRepository: IPluginRepository,
|
||||||
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||||
|
) {
|
||||||
|
this.access = AccessCore.create(accessRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
search(auth: AuthDto, dto: SearchPluginDto) {
|
||||||
|
// await this.access.requirePermission(authUser, Permission.PLUGIN_READ);
|
||||||
|
return this.pluginRepository.search(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
async import(auth: AuthDto, dto: PluginImportDto) {
|
||||||
|
// await this.access.requirePermission(authUser, Permission.PLUGIN_ADMIN);
|
||||||
|
if (dto.url.startsWith('http')) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginPath = '/path/to/plugin';
|
||||||
|
await this.pluginRepository.download(dto.url, pluginPath);
|
||||||
|
const { version, id, name, description } = await this.load(pluginPath);
|
||||||
|
|
||||||
|
const response = await this.pluginRepository.create({
|
||||||
|
version,
|
||||||
|
packageId: id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
isInstalled: false,
|
||||||
|
isEnabled: dto.isEnabled,
|
||||||
|
isTrusted: false,
|
||||||
|
installPath: pluginPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapPlugin(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async install(auth: AuthDto, id: string) {
|
||||||
|
await this.access.requirePermission(auth, Permission.PLUGIN_INSTALL, id);
|
||||||
|
// TODO
|
||||||
|
return this.pluginRepository.update({ id, isInstalled: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async uninstall(auth: AuthDto, id: string) {
|
||||||
|
await this.access.requirePermission(auth, Permission.PLUGIN_UNINSTALL, id);
|
||||||
|
// TODO
|
||||||
|
return this.pluginRepository.update({ id, isInstalled: false, installPath: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(auth: AuthDto, id: string, dto: PluginUpdateDto) {
|
||||||
|
await this.access.requirePermission(auth, Permission.PLUGIN_WRITE, id);
|
||||||
|
return this.pluginRepository.update({
|
||||||
|
id,
|
||||||
|
isEnabled: dto.isEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(auth: AuthDto, id: string) {
|
||||||
|
await this.access.requirePermission(auth, Permission.PLUGIN_WRITE, id);
|
||||||
|
await this.findOrFail(id);
|
||||||
|
await this.pluginRepository.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findOrFail(id: string) {
|
||||||
|
const plugin = await this.pluginRepository.get(id);
|
||||||
|
if (!plugin) {
|
||||||
|
throw new BadRequestException('Plugin not found');
|
||||||
|
}
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO security implications
|
||||||
|
private async load(pluginPath: string): Promise<Plugin> {
|
||||||
|
const pluginLike = await this.pluginRepository.load(pluginPath);
|
||||||
|
let plugin: Plugin | undefined;
|
||||||
|
|
||||||
|
const pluginModule = pluginLike as { default: Plugin };
|
||||||
|
if (pluginModule.default) {
|
||||||
|
plugin = pluginModule.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginExport = pluginLike as { plugin: Plugin };
|
||||||
|
if (pluginExport.plugin) {
|
||||||
|
plugin = pluginExport.plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginFactory = pluginLike as PluginFactory;
|
||||||
|
if (pluginFactory) {
|
||||||
|
plugin = await pluginFactory.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use class-validator
|
||||||
|
const isPlugin = plugin && !!plugin.version && Array.isArray(plugin.actions);
|
||||||
|
if (isPlugin) {
|
||||||
|
return plugin as Plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unable to load plugin');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user