Compare commits

..

3 Commits

Author SHA1 Message Date
martin a20ff86591 Update web/src/lib/components/photos-page/asset-grid.svelte
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-05-27 21:02:08 +02:00
martin 43aa1e8162 Update web/src/lib/actions/focus.ts
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-05-27 21:02:01 +02:00
martabal 683f503647 feat: use shortcut to favorite and archive on asset-grid 2024-05-25 12:58:49 +02:00
189 changed files with 2777 additions and 4572 deletions
+3 -3
View File
@@ -1,4 +1,4 @@
import { getMyUser } from '@immich/sdk'; import { getMyUserInfo } from '@immich/sdk';
import { existsSync } from 'node:fs'; import { existsSync } from 'node:fs';
import { mkdir, unlink } from 'node:fs/promises'; import { mkdir, unlink } from 'node:fs/promises';
import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils'; import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils';
@@ -10,13 +10,13 @@ export const login = async (url: string, key: string, options: BaseOptions) => {
await connect(url, key); await connect(url, key);
const [error, user] = await withError(getMyUser()); const [error, userInfo] = await withError(getMyUserInfo());
if (error) { if (error) {
logError(error, 'Failed to load user info'); logError(error, 'Failed to load user info');
process.exit(1); process.exit(1);
} }
console.log(`Logged in as ${user.email}`); console.log(`Logged in as ${userInfo.email}`);
if (!existsSync(configDir)) { if (!existsSync(configDir)) {
// Create config folder if it doesn't exist // Create config folder if it doesn't exist
+2 -2
View File
@@ -1,4 +1,4 @@
import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk'; import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
import { BaseOptions, authenticate } from 'src/utils'; import { BaseOptions, authenticate } from 'src/utils';
export const serverInfo = async (options: BaseOptions) => { export const serverInfo = async (options: BaseOptions) => {
@@ -8,7 +8,7 @@ export const serverInfo = async (options: BaseOptions) => {
getServerVersion(), getServerVersion(),
getSupportedMediaTypes(), getSupportedMediaTypes(),
getAssetStatistics({}), getAssetStatistics({}),
getMyUser(), getMyUserInfo(),
]); ]);
console.log(`Server Info (via ${userInfo.email})`); console.log(`Server Info (via ${userInfo.email})`);
+2 -2
View File
@@ -1,4 +1,4 @@
import { getMyUser, init, isHttpError } from '@immich/sdk'; import { getMyUserInfo, init, isHttpError } from '@immich/sdk';
import { glob } from 'fast-glob'; import { glob } from 'fast-glob';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs'; import { createReadStream } from 'node:fs';
@@ -48,7 +48,7 @@ export const connect = async (url: string, key: string) => {
init({ baseUrl: url, apiKey: key }); init({ baseUrl: url, apiKey: key });
const [error] = await withError(getMyUser()); const [error] = await withError(getMyUserInfo());
if (isHttpError(error)) { if (isHttpError(error)) {
logError(error, 'Failed to connect to server'); logError(error, 'Failed to connect to server');
process.exit(1); process.exit(1);
-3
View File
@@ -9,9 +9,6 @@ services:
container_name: immich_server container_name: immich_server
command: ['/usr/src/app/bin/immich-dev'] command: ['/usr/src/app/bin/immich-dev']
image: immich-server-dev:latest image: immich-server-dev:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
-3
View File
@@ -4,9 +4,6 @@ services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
image: immich-server:latest image: immich-server:latest
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
-3
View File
@@ -12,9 +12,6 @@ services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

+1 -37
View File
@@ -110,44 +110,8 @@ Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to
## Example Configuration ## Example Configuration
<details>
<summary>Authentik Example</summary>
### Authentik Example
Here's an example of OAuth configured for Authentik: Here's an example of OAuth configured for Authentik:
<img src={require('./img/oauth-settings.png').default} title="OAuth settings" /> ![OAuth Settings](./img/oauth-settings.png)
</details>
<details>
<summary>Google Example</summary>
### Google Example
Configuration of Authorised redirect URIs (Google Console)
<img src={require('./img/google-example.webp').default} width='50%' title="Authorised redirect URIs" />
Configuration of OAuth in System Settings
| Setting | Value |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| Issuer URL | [https://accounts.google.com](https://accounts.google.com) |
| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com |
| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Google (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled |
| Mobile Redirect URI Override | Enabled (required) |
| Mobile Redirect URI | [https://demo.immich.app/api/oauth/mobile-redirect](https://demo.immich.app/api/oauth/mobile-redirect) |
</details>
[oidc]: https://openid.net/connect/ [oidc]: https://openid.net/connect/
+1 -1
View File
@@ -15,7 +15,7 @@ The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/downlo
::: :::
```yaml ```yaml
name: immich_remote_ml version: '3.8'
services: services:
immich-machine-learning: immich-machine-learning:
+2 -2
View File
@@ -4,7 +4,7 @@ import {
AlbumUserRole, AlbumUserRole,
AssetFileUploadResponseDto, AssetFileUploadResponseDto,
AssetOrder, AssetOrder,
deleteUserAdmin, deleteUser,
getAlbumInfo, getAlbumInfo,
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
@@ -107,7 +107,7 @@ describe('/albums', () => {
}), }),
]); ]);
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }); await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
}); });
describe('GET /albums', () => { describe('GET /albums', () => {
+21 -58
View File
@@ -5,7 +5,7 @@ import {
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
getAssetInfo, getAssetInfo,
getMyUser, getMyUserInfo,
updateAssets, updateAssets,
} from '@immich/sdk'; } from '@immich/sdk';
import { exiftool } from 'exiftool-vendored'; import { exiftool } from 'exiftool-vendored';
@@ -86,8 +86,6 @@ describe('/asset', () => {
utils.userSetup(admin.accessToken, createUserDto.create('stack')), utils.userSetup(admin.accessToken, createUserDto.create('stack')),
]); ]);
await utils.createPartner(user1.accessToken, user2.userId);
// asset location // asset location
locationAsset = await utils.createAsset(admin.accessToken, { locationAsset = await utils.createAsset(admin.accessToken, {
assetData: { assetData: {
@@ -215,18 +213,15 @@ describe('/asset', () => {
expect(body).toMatchObject({ expect(body).toMatchObject({
id: user1Assets[0].id, id: user1Assets[0].id,
isFavorite: false, isFavorite: false,
people: { people: [
visiblePeople: [ {
{ birthDate: null,
birthDate: null, id: expect.any(String),
id: expect.any(String), isHidden: false,
isHidden: false, name: 'Test Person',
name: 'Test Person', thumbnailPath: '/my/awesome/thumbnail.jpg',
thumbnailPath: '/my/awesome/thumbnail.jpg', },
}, ],
],
numberOfFaces: 1,
},
}); });
const sharedLink = await utils.createSharedLink(user1.accessToken, { const sharedLink = await utils.createSharedLink(user1.accessToken, {
@@ -236,36 +231,7 @@ describe('/asset', () => {
const data = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`); const data = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
expect(data.status).toBe(200); expect(data.status).toBe(200);
expect(data.body).not.toHaveProperty('people'); expect(data.body).toMatchObject({ people: [] });
});
describe('partner assets', () => {
it('should get the asset info', async () => {
const { status, body } = await request(app)
.get(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
});
it('disallows viewing archived assets', async () => {
const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
const { status } = await request(app)
.get(`/asset/${asset.id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(400);
});
it('disallows viewing trashed assets', async () => {
const asset = await utils.createAsset(user1.accessToken);
await utils.deleteAssets(user1.accessToken, [asset.id]);
const { status } = await request(app)
.get(`/asset/${asset.id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(400);
});
}); });
}); });
@@ -514,18 +480,15 @@ describe('/asset', () => {
expect(body).toMatchObject({ expect(body).toMatchObject({
id: user1Assets[0].id, id: user1Assets[0].id,
isFavorite: true, isFavorite: true,
people: { people: [
visiblePeople: [ {
{ birthDate: null,
birthDate: null, id: expect.any(String),
id: expect.any(String), isHidden: false,
isHidden: false, name: 'Test Person',
name: 'Test Person', thumbnailPath: '/my/awesome/thumbnail.jpg',
thumbnailPath: '/my/awesome/thumbnail.jpg', },
}, ],
],
numberOfFaces: 1,
},
}); });
}); });
}); });
@@ -1168,7 +1131,7 @@ describe('/asset', () => {
expect(body).toEqual({ id: expect.any(String), duplicate: false }); expect(body).toEqual({ id: expect.any(String), duplicate: false });
expect(status).toBe(201); expect(status).toBe(201);
const user = await getMyUser({ headers: asBearerAuth(quotaUser.accessToken) }); const user = await getMyUserInfo({ headers: asBearerAuth(quotaUser.accessToken) });
expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 })); expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
}); });
+2 -2
View File
@@ -5,7 +5,7 @@ import {
SharedLinkResponseDto, SharedLinkResponseDto,
SharedLinkType, SharedLinkType,
createAlbum, createAlbum,
deleteUserAdmin, deleteUser,
} from '@immich/sdk'; } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@@ -86,7 +86,7 @@ describe('/shared-links', () => {
}), }),
]); ]);
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }); await deleteUser({ id: user2.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
}); });
describe('GET /share/${key}', () => { describe('GET /share/${key}', () => {
-317
View File
@@ -1,317 +0,0 @@
import { LoginResponseDto, deleteUserAdmin, getMyUser, getUserAdmin, login } from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/admin/users', () => {
let websocket: Socket;
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let userToHardDelete: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[websocket, nonAdmin, deletedUser, userToDelete, userToHardDelete] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]);
await deleteUserAdmin(
{ id: deletedUser.userId, userAdminDeleteDto: {} },
{ headers: asBearerAuth(admin.accessToken) },
);
});
afterAll(() => {
utils.disconnectWebsocket(websocket);
});
describe('GET /admin/users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/admin/users`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.get(`/admin/users`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should hide deleted users by default', async () => {
const { status, body } = await request(app)
.get(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: admin.userEmail }),
expect.objectContaining({ email: nonAdmin.userEmail }),
expect.objectContaining({ email: userToDelete.userEmail }),
expect.objectContaining({ email: userToHardDelete.userEmail }),
]),
);
});
it('should include deleted users', async () => {
const { status, body } = await request(app)
.get(`/admin/users?withDeleted=true`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: admin.userEmail }),
expect.objectContaining({ email: nonAdmin.userEmail }),
expect.objectContaining({ email: userToDelete.userEmail }),
expect.objectContaining({ email: userToHardDelete.userEmail }),
expect.objectContaining({ email: deletedUser.userEmail }),
]),
);
});
});
describe('POST /admin/users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/admin/users`).send(createUserDto.user1);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send(createUserDto.user1);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
for (const key of [
'password',
'email',
'name',
'quotaSizeInBytes',
'shouldChangePassword',
'memoriesEnabled',
'notify',
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should ignore `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.send({
isAdmin: true,
email: 'user5@immich.cloud',
password: 'password123',
name: 'Immich',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'user5@immich.cloud',
isAdmin: false,
shouldChangePassword: true,
});
expect(status).toBe(201);
});
it('should create a user without memories enabled', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.send({
email: 'no-memories@immich.cloud',
password: 'Password123',
name: 'No Memories',
memoriesEnabled: false,
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'no-memories@immich.cloud',
memoriesEnabled: false,
});
expect(status).toBe(201);
});
});
describe('PUT /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/admin/users/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
for (const key of ['password', 'email', 'name', 'shouldChangePassword', 'memoriesEnabled']) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should not allow a non-admin to become an admin', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${nonAdmin.userId}`)
.send({ isAdmin: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ isAdmin: false });
});
it('ignores updates to profileImagePath', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ profileImagePath: 'invalid.jpg' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
});
it('should update first and last name', async () => {
const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ name: 'Name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...before,
updatedAt: expect.any(String),
name: 'Name',
});
expect(before.updatedAt).not.toEqual(body.updatedAt);
});
it('should update memories enabled', async () => {
const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ memoriesEnabled: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
...before,
updatedAt: expect.anything(),
memoriesEnabled: false,
});
expect(before.updatedAt).not.toEqual(body.updatedAt);
});
it('should update password', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${nonAdmin.userId}`)
.send({ password: 'super-secret' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ email: nonAdmin.userEmail });
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
expect(token.accessToken).toBeDefined();
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
expect(user).toMatchObject({ email: nonAdmin.userEmail });
});
});
describe('DELETE /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should delete user', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
});
it('should hard delete a user', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToHardDelete.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToHardDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
});
});
describe('POST /admin/users/:id/restore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/admin/users/${userToDelete.userId}/restore`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { status, body } = await request(app)
.post(`/admin/users/${userToDelete.userId}/restore`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
});
});
+206 -109
View File
@@ -1,28 +1,37 @@
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyUser, login } from '@immich/sdk'; import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
import { createUserDto } from 'src/fixtures'; import { Socket } from 'socket.io-client';
import { createUserDto, userDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/users', () => { describe('/users', () => {
let websocket: Socket;
let admin: LoginResponseDto; let admin: LoginResponseDto;
let deletedUser: LoginResponseDto; let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let userToHardDelete: LoginResponseDto;
let nonAdmin: LoginResponseDto; let nonAdmin: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {
await utils.resetDatabase(); await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false }); admin = await utils.adminSetup({ onboarding: false });
[deletedUser, nonAdmin] = await Promise.all([ [websocket, deletedUser, nonAdmin, userToDelete, userToHardDelete] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.user1), utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2), utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]); ]);
await deleteUserAdmin( await deleteUser({ id: deletedUser.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
{ id: deletedUser.userId, userAdminDeleteDto: {} }, });
{ headers: asBearerAuth(admin.accessToken) },
); afterAll(() => {
utils.disconnectWebsocket(websocket);
}); });
describe('GET /users', () => { describe('GET /users', () => {
@@ -35,14 +44,71 @@ describe('/users', () => {
it('should get users', async () => { it('should get users', async () => {
const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toHaveLength(2); expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]),
);
});
it('should hide deleted users', async () => {
const { status, body } = await request(app)
.get(`/users`)
.query({ isAll: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]), ]),
); );
}); });
it('should include deleted users', async () => {
const { status, body } = await request(app)
.get(`/users`)
.query({ isAll: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]),
);
});
});
describe('GET /users/:id', () => {
it('should require authentication', async () => {
const { status } = await request(app).get(`/users/${admin.userId}`);
expect(status).toEqual(401);
});
it('should get the user info', async () => {
const { status, body } = await request(app)
.get(`/users/${admin.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
});
});
}); });
describe('GET /users/me', () => { describe('GET /users/me', () => {
@@ -52,54 +118,154 @@ describe('/users', () => {
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should not work for shared links', async () => { it('should get my info', async () => {
const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' });
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
});
const { status, body } = await request(app).get(`/users/me?key=${sharedLink.key}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should get my user', async () => {
const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toMatchObject({ expect(body).toMatchObject({
id: admin.userId, id: admin.userId,
email: 'admin@immich.cloud', email: 'admin@immich.cloud',
memoriesEnabled: true,
quotaUsageInBytes: 0,
}); });
}); });
}); });
describe('PUT /users/me', () => { describe('POST /users', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/users/me`); const { status, body } = await request(app).post(`/users`).send(createUserDto.user1);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
for (const key of ['email', 'name', 'memoriesEnabled', 'avatarColor']) { for (const key of Object.keys(createUserDto.user1)) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const dto = { [key]: null };
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me`) .post(`/users`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send(dto); .send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest()); expect(body).toEqual(errorDto.badRequest());
}); });
} }
it('should ignore `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/users`)
.send({
isAdmin: true,
email: 'user5@immich.cloud',
password: 'password123',
name: 'Immich',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'user5@immich.cloud',
isAdmin: false,
shouldChangePassword: true,
});
expect(status).toBe(201);
});
it('should create a user without memories enabled', async () => {
const { status, body } = await request(app)
.post(`/users`)
.send({
email: 'no-memories@immich.cloud',
password: 'Password123',
name: 'No Memories',
memoriesEnabled: false,
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'no-memories@immich.cloud',
memoriesEnabled: false,
});
expect(status).toBe(201);
});
});
describe('DELETE /users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/users/${userToDelete.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should delete user', async () => {
const { status, body } = await request(app)
.delete(`/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
});
it('should hard delete user', async () => {
const { status, body } = await request(app)
.delete(`/users/${userToHardDelete.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToHardDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
});
});
describe('PUT /users', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/users`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of Object.keys(userDto.admin)) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/users`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...userDto.admin, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it('should not allow a non-admin to become an admin', async () => {
const { status, body } = await request(app)
.put(`/users`)
.send({ isAdmin: true, id: nonAdmin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.alreadyHasAdmin);
});
it('ignores updates to profileImagePath', async () => {
const { status, body } = await request(app)
.put(`/users`)
.send({ id: admin.userId, profileImagePath: 'invalid.jpg' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
});
it('should update first and last name', async () => { it('should update first and last name', async () => {
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me`) .put(`/users`)
.send({ name: 'Name' }) .send({
id: admin.userId,
name: 'Name',
})
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -108,13 +274,17 @@ describe('/users', () => {
updatedAt: expect.any(String), updatedAt: expect.any(String),
name: 'Name', name: 'Name',
}); });
expect(before.updatedAt).not.toEqual(body.updatedAt);
}); });
it('should update memories enabled', async () => { it('should update memories enabled', async () => {
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me`) .put(`/users`)
.send({ memoriesEnabled: false }) .send({
id: admin.userId,
memoriesEnabled: false,
})
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -123,80 +293,7 @@ describe('/users', () => {
updatedAt: expect.anything(), updatedAt: expect.anything(),
memoriesEnabled: false, memoriesEnabled: false,
}); });
expect(before.updatedAt).not.toEqual(body.updatedAt);
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
expect(after.memoriesEnabled).toBe(false);
});
/** @deprecated */
it('should allow a user to change their password (deprecated)', async () => {
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
expect(user.shouldChangePassword).toBe(true);
const { status, body } = await request(app)
.put(`/users/me`)
.send({ password: 'super-secret' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
email: nonAdmin.userEmail,
shouldChangePassword: false,
});
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
expect(token.accessToken).toBeDefined();
});
it('should not allow user to change to a taken email', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ email: 'admin@immich.cloud' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(400);
expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
});
it('should update my email', async () => {
const before = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
const { status, body } = await request(app)
.put(`/users/me`)
.send({ email: 'non-admin@immich.cloud' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
...before,
email: 'non-admin@immich.cloud',
updatedAt: expect.anything(),
});
});
});
describe('GET /users/:id', () => {
it('should require authentication', async () => {
const { status } = await request(app).get(`/users/${admin.userId}`);
expect(status).toEqual(401);
});
it('should get the user', async () => {
const { status, body } = await request(app)
.get(`/users/${admin.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
});
expect(body).not.toMatchObject({
shouldChangePassword: expect.anything(),
memoriesEnabled: expect.anything(),
storageLabel: expect.anything(),
});
}); });
}); });
}); });
+4 -7
View File
@@ -5,18 +5,17 @@ import {
CreateAlbumDto, CreateAlbumDto,
CreateAssetDto, CreateAssetDto,
CreateLibraryDto, CreateLibraryDto,
CreateUserDto,
MetadataSearchDto, MetadataSearchDto,
PersonCreateDto, PersonCreateDto,
SharedLinkCreateDto, SharedLinkCreateDto,
UserAdminCreateDto,
ValidateLibraryDto, ValidateLibraryDto,
createAlbum, createAlbum,
createApiKey, createApiKey,
createLibrary, createLibrary,
createPartner,
createPerson, createPerson,
createSharedLink, createSharedLink,
createUserAdmin, createUser,
deleteAssets, deleteAssets,
getAllJobsStatus, getAllJobsStatus,
getAssetInfo, getAssetInfo,
@@ -273,8 +272,8 @@ export const utils = {
return response; return response;
}, },
userSetup: async (accessToken: string, dto: UserAdminCreateDto) => { userSetup: async (accessToken: string, dto: CreateUserDto) => {
await createUserAdmin({ userAdminCreateDto: dto }, { headers: asBearerAuth(accessToken) }); await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
return login({ return login({
loginCredentialDto: { email: dto.email, password: dto.password }, loginCredentialDto: { email: dto.email, password: dto.password },
}); });
@@ -386,8 +385,6 @@ export const utils = {
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) => validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
setAuthCookies: async (context: BrowserContext, accessToken: string) => setAuthCookies: async (context: BrowserContext, accessToken: string) =>
await context.addCookies([ await context.addCookies([
{ {
@@ -43,18 +43,4 @@ test.describe('Detail Panel', () => {
await page.keyboard.press('i'); await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0); await expect(page.locator('#detail-panel')).toHaveCount(0);
}); });
test('description is visible for owner on shared links', async ({ context, page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
const textarea = page.getByRole('textbox', { name: 'Add a description' });
await page.getByRole('button', { name: 'Info' }).click();
await expect(textarea).toBeVisible();
await expect(textarea).not.toBeDisabled();
});
}); });
+7 -7
View File
@@ -27,7 +27,7 @@ class User {
Id get isarId => fastHash(id); Id get isarId => fastHash(id);
User.fromUserDto(UserAdminResponseDto dto) User.fromUserDto(UserResponseDto dto)
: id = dto.id, : id = dto.id,
updatedAt = dto.updatedAt, updatedAt = dto.updatedAt,
email = dto.email, email = dto.email,
@@ -44,21 +44,21 @@ class User {
User.fromPartnerDto(PartnerResponseDto dto) User.fromPartnerDto(PartnerResponseDto dto)
: id = dto.id, : id = dto.id,
updatedAt = DateTime.now(), updatedAt = dto.updatedAt,
email = dto.email, email = dto.email,
name = dto.name, name = dto.name,
isPartnerSharedBy = false, isPartnerSharedBy = false,
isPartnerSharedWith = false, isPartnerSharedWith = false,
profileImagePath = dto.profileImagePath, profileImagePath = dto.profileImagePath,
isAdmin = false, isAdmin = dto.isAdmin,
memoryEnabled = false, memoryEnabled = dto.memoriesEnabled ?? false,
avatarColor = dto.avatarColor.toAvatarColor(), avatarColor = dto.avatarColor.toAvatarColor(),
inTimeline = dto.inTimeline ?? false, inTimeline = dto.inTimeline ?? false,
quotaUsageInBytes = 0, quotaUsageInBytes = dto.quotaUsageInBytes ?? 0,
quotaSizeInBytes = 0; quotaSizeInBytes = dto.quotaSizeInBytes ?? 0;
/// Base user dto used where the complete user object is not required /// Base user dto used where the complete user object is not required
User.fromSimpleUserDto(UserResponseDto dto) User.fromSimpleUserDto(UserDto dto)
: id = dto.id, : id = dto.id,
email = dto.email, email = dto.email,
name = dto.name, name = dto.name,
+16 -26
View File
@@ -133,7 +133,7 @@ class AlbumViewerPage extends HookConsumerWidget {
Widget buildTitle(Album album) { Widget buildTitle(Album album) {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 8, right: 8), padding: const EdgeInsets.only(left: 8, right: 8, top: 24),
child: userId == album.ownerId && album.isRemote child: userId == album.ownerId && album.isRemote
? AlbumViewerEditableTitle( ? AlbumViewerEditableTitle(
album: album, album: album,
@@ -228,30 +228,9 @@ class AlbumViewerPage extends HookConsumerWidget {
} }
return Scaffold( return Scaffold(
body: Stack( appBar: ref.watch(multiselectProvider)
children: [ ? null
album.widgetWhen( : album.when(
onData: (data) => MultiselectGrid(
renderListProvider: albumRenderlistProvider(albumId),
topWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildHeader(data),
if (data.isRemote) buildControlButton(data),
],
),
onRemoveFromAlbum: onRemoveFromAlbumPressed,
editEnabled: data.ownerId == userId,
),
),
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
top: ref.watch(multiselectProvider)
? -(kToolbarHeight + MediaQuery.of(context).padding.top)
: 0,
left: 0,
right: 0,
child: album.when(
data: (data) => AlbumViewerAppbar( data: (data) => AlbumViewerAppbar(
titleFocusNode: titleFocusNode, titleFocusNode: titleFocusNode,
album: data, album: data,
@@ -263,8 +242,19 @@ class AlbumViewerPage extends HookConsumerWidget {
error: (error, stackTrace) => AppBar(title: const Text("Error")), error: (error, stackTrace) => AppBar(title: const Text("Error")),
loading: () => AppBar(), loading: () => AppBar(),
), ),
body: album.widgetWhen(
onData: (data) => MultiselectGrid(
renderListProvider: albumRenderlistProvider(albumId),
topWidget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildHeader(data),
if (data.isRemote) buildControlButton(data),
],
), ),
], onRemoveFromAlbum: onRemoveFromAlbumPressed,
editEnabled: data.ownerId == userId,
),
), ),
); );
} }
@@ -138,9 +138,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Future<bool> changePassword(String newPassword) async { Future<bool> changePassword(String newPassword) async {
try { try {
await _apiService.userApi.updateMyUser( await _apiService.userApi.updateUser(
UserUpdateMeDto( UpdateUserDto(
id: state.userId,
password: newPassword, password: newPassword,
shouldChangePassword: false,
), ),
); );
@@ -176,9 +178,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
user = offlineUser; user = offlineUser;
retResult = false; retResult = false;
} else { } else {
UserAdminResponseDto? userResponseDto; UserResponseDto? userResponseDto;
try { try {
userResponseDto = await _apiService.userApi.getMyUser(); userResponseDto = await _apiService.userApi.getMyUserInfo();
} on ApiException catch (error, stackTrace) { } on ApiException catch (error, stackTrace) {
_log.severe( _log.severe(
"Error getting user information from the server [API EXCEPTION]", "Error getting user information from the server [API EXCEPTION]",
+1 -1
View File
@@ -20,7 +20,7 @@ class CurrentUserProvider extends StateNotifier<User?> {
refresh() async { refresh() async {
try { try {
final user = await _apiService.userApi.getMyUser(); final user = await _apiService.userApi.getMyUserInfo();
if (user != null) { if (user != null) {
Store.put( Store.put(
StoreKey.currentUser, StoreKey.currentUser,
@@ -57,7 +57,7 @@ class TabNavigationObserver extends AutoRouterObserver {
// Update user info // Update user info
try { try {
final userResponseDto = final userResponseDto =
await ref.read(apiServiceProvider).userApi.getMyUser(); await ref.read(apiServiceProvider).userApi.getMyUserInfo();
if (userResponseDto == null) { if (userResponseDto == null) {
return; return;
+1 -1
View File
@@ -84,7 +84,7 @@ class AssetService {
final AssetResponseDto? dto = final AssetResponseDto? dto =
await _apiService.assetApi.getAssetInfo(remoteId); await _apiService.assetApi.getAssetInfo(remoteId);
return dto?.people?.visiblePeople; return dto?.people;
} catch (error, stack) { } catch (error, stack) {
log.severe( log.severe(
'Error while getting remote asset info: ${error.toString()}', 'Error while getting remote asset info: ${error.toString()}',
+4 -4
View File
@@ -37,10 +37,10 @@ class UserService {
this._partnerService, this._partnerService,
); );
Future<List<User>?> _getAllUsers() async { Future<List<User>?> _getAllUsers({required bool isAll}) async {
try { try {
final dto = await _apiService.userApi.searchUsers(); final dto = await _apiService.userApi.getAllUsers(isAll);
return dto?.map(User.fromSimpleUserDto).toList(); return dto?.map(User.fromUserDto).toList();
} catch (e) { } catch (e) {
_log.warning("Failed get all users", e); _log.warning("Failed get all users", e);
return null; return null;
@@ -71,7 +71,7 @@ class UserService {
} }
Future<List<User>?> getUsersFromServer() async { Future<List<User>?> getUsersFromServer() async {
final List<User>? users = await _getAllUsers(); final List<User>? users = await _getAllUsers(isAll: true);
final List<User>? sharedBy = final List<User>? sharedBy =
await _partnerService.getPartners(PartnerDirection.sharedBy); await _partnerService.getPartners(PartnerDirection.sharedBy);
final List<User>? sharedWith = final List<User>? sharedWith =
@@ -36,62 +36,58 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
[], [],
); );
return Material( return TextField(
color: Colors.transparent, onChanged: (value) {
child: TextField( if (value.isEmpty) {
onChanged: (value) { } else {
if (value.isEmpty) { ref.watch(albumViewerProvider.notifier).setEditTitleText(value);
} else { }
ref.watch(albumViewerProvider.notifier).setEditTitleText(value); },
} focusNode: titleFocusNode,
}, style: context.textTheme.headlineMedium,
focusNode: titleFocusNode, controller: titleTextEditController,
style: context.textTheme.headlineMedium, onTap: () {
controller: titleTextEditController, FocusScope.of(context).requestFocus(titleFocusNode);
onTap: () {
FocusScope.of(context).requestFocus(titleFocusNode);
ref.watch(albumViewerProvider.notifier).setEditTitleText(album.name); ref.watch(albumViewerProvider.notifier).setEditTitleText(album.name);
ref.watch(albumViewerProvider.notifier).enableEditAlbum(); ref.watch(albumViewerProvider.notifier).enableEditAlbum();
if (titleTextEditController.text == 'Untitled') { if (titleTextEditController.text == 'Untitled') {
titleTextEditController.clear(); titleTextEditController.clear();
} }
}, },
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
const EdgeInsets.symmetric(horizontal: 8, vertical: 8), suffixIcon: titleFocusNode.hasFocus
suffixIcon: titleFocusNode.hasFocus ? IconButton(
? IconButton( onPressed: () {
onPressed: () { titleTextEditController.clear();
titleTextEditController.clear(); },
}, icon: Icon(
icon: Icon( Icons.cancel_rounded,
Icons.cancel_rounded, color: context.primaryColor,
color: context.primaryColor, ),
), splashRadius: 10,
splashRadius: 10, )
) : null,
: null, enabledBorder: OutlineInputBorder(
enabledBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.transparent),
borderSide: const BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10), ),
), focusedBorder: OutlineInputBorder(
focusedBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.transparent),
borderSide: const BorderSide(color: Colors.transparent), borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(10), ),
), focusColor: Colors.grey[300],
focusColor: Colors.grey[300], fillColor: context.isDarkTheme
fillColor: context.isDarkTheme ? const Color.fromARGB(255, 32, 33, 35)
? const Color.fromARGB(255, 32, 33, 35) : Colors.grey[200],
: Colors.grey[200], filled: titleFocusNode.hasFocus,
filled: titleFocusNode.hasFocus, hintText: 'share_add_title'.tr(),
hintText: 'share_add_title'.tr(), hintStyle: TextStyle(
hintStyle: TextStyle( fontSize: 28,
fontSize: 28, color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700],
color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700], fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold,
),
), ),
), ),
); );
@@ -238,10 +238,8 @@ class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
} }
bool appBarOffset() { bool appBarOffset() {
return (ref.watch(tabProvider).index == 0 && return ref.watch(tabProvider).index == 0 &&
ModalRoute.of(context)?.settings.name == ModalRoute.of(context)?.settings.name == TabControllerRoute.name;
TabControllerRoute.name) ||
(ModalRoute.of(context)?.settings.name == AlbumViewerRoute.name);
} }
final listWidget = ScrollablePositionedList.builder( final listWidget = ScrollablePositionedList.builder(
+11 -18
View File
@@ -122,7 +122,6 @@ Class | Method | HTTP request | Description
*DuplicateApi* | [**getAssetDuplicates**](doc//DuplicateApi.md#getassetduplicates) | **GET** /duplicates | *DuplicateApi* | [**getAssetDuplicates**](doc//DuplicateApi.md#getassetduplicates) | **GET** /duplicates |
*FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /faces | *FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /faces |
*FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /faces/{id} | *FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /faces/{id} |
*FaceApi* | [**unassignFace**](doc//FaceApi.md#unassignface) | **DELETE** /faces/{id} |
*FileReportApi* | [**fixAuditFiles**](doc//FileReportApi.md#fixauditfiles) | **POST** /reports/fix | *FileReportApi* | [**fixAuditFiles**](doc//FileReportApi.md#fixauditfiles) | **POST** /reports/fix |
*FileReportApi* | [**getAuditFiles**](doc//FileReportApi.md#getauditfiles) | **GET** /reports | *FileReportApi* | [**getAuditFiles**](doc//FileReportApi.md#getauditfiles) | **GET** /reports |
*FileReportApi* | [**getFileChecksums**](doc//FileReportApi.md#getfilechecksums) | **POST** /reports/checksum | *FileReportApi* | [**getFileChecksums**](doc//FileReportApi.md#getfilechecksums) | **POST** /reports/checksum |
@@ -161,7 +160,6 @@ Class | Method | HTTP request | Description
*PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /people/{id}/thumbnail | *PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /people/{id}/thumbnail |
*PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /people/{id}/merge | *PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /people/{id}/merge |
*PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /people/{id}/reassign | *PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /people/{id}/reassign |
*PersonApi* | [**unassignFaces**](doc//PersonApi.md#unassignfaces) | **DELETE** /people |
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /people | *PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /people |
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /people/{id} | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /people/{id} |
*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | *SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities |
@@ -214,18 +212,15 @@ Class | Method | HTTP request | Description
*TrashApi* | [**restoreAssets**](doc//TrashApi.md#restoreassets) | **POST** /trash/restore/assets | *TrashApi* | [**restoreAssets**](doc//TrashApi.md#restoreassets) | **POST** /trash/restore/assets |
*TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore | *TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore |
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /users/profile-image | *UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /users/profile-image |
*UserApi* | [**createUserAdmin**](doc//UserApi.md#createuseradmin) | **POST** /admin/users | *UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /users |
*UserApi* | [**deleteProfileImage**](doc//UserApi.md#deleteprofileimage) | **DELETE** /users/profile-image | *UserApi* | [**deleteProfileImage**](doc//UserApi.md#deleteprofileimage) | **DELETE** /users/profile-image |
*UserApi* | [**deleteUserAdmin**](doc//UserApi.md#deleteuseradmin) | **DELETE** /admin/users/{id} | *UserApi* | [**deleteUser**](doc//UserApi.md#deleteuser) | **DELETE** /users/{id} |
*UserApi* | [**getMyUser**](doc//UserApi.md#getmyuser) | **GET** /users/me | *UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /users |
*UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /users/me |
*UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /users/{id}/profile-image | *UserApi* | [**getProfileImage**](doc//UserApi.md#getprofileimage) | **GET** /users/{id}/profile-image |
*UserApi* | [**getUser**](doc//UserApi.md#getuser) | **GET** /users/{id} | *UserApi* | [**getUserById**](doc//UserApi.md#getuserbyid) | **GET** /users/{id} |
*UserApi* | [**getUserAdmin**](doc//UserApi.md#getuseradmin) | **GET** /admin/users/{id} | *UserApi* | [**restoreUser**](doc//UserApi.md#restoreuser) | **POST** /users/{id}/restore |
*UserApi* | [**restoreUserAdmin**](doc//UserApi.md#restoreuseradmin) | **POST** /admin/users/{id}/restore | *UserApi* | [**updateUser**](doc//UserApi.md#updateuser) | **PUT** /users |
*UserApi* | [**searchUsers**](doc//UserApi.md#searchusers) | **GET** /users |
*UserApi* | [**searchUsersAdmin**](doc//UserApi.md#searchusersadmin) | **GET** /admin/users |
*UserApi* | [**updateMyUser**](doc//UserApi.md#updatemyuser) | **PUT** /users/me |
*UserApi* | [**updateUserAdmin**](doc//UserApi.md#updateuseradmin) | **PUT** /admin/users/{id} |
## Documentation For Models ## Documentation For Models
@@ -285,6 +280,8 @@ Class | Method | HTTP request | Description
- [CreateLibraryDto](doc//CreateLibraryDto.md) - [CreateLibraryDto](doc//CreateLibraryDto.md)
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
- [CreateTagDto](doc//CreateTagDto.md) - [CreateTagDto](doc//CreateTagDto.md)
- [CreateUserDto](doc//CreateUserDto.md)
- [DeleteUserDto](doc//DeleteUserDto.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
- [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadInfoDto](doc//DownloadInfoDto.md)
- [DownloadResponseDto](doc//DownloadResponseDto.md) - [DownloadResponseDto](doc//DownloadResponseDto.md)
@@ -331,7 +328,6 @@ Class | Method | HTTP request | Description
- [PeopleResponseDto](doc//PeopleResponseDto.md) - [PeopleResponseDto](doc//PeopleResponseDto.md)
- [PeopleUpdateDto](doc//PeopleUpdateDto.md) - [PeopleUpdateDto](doc//PeopleUpdateDto.md)
- [PeopleUpdateItem](doc//PeopleUpdateItem.md) - [PeopleUpdateItem](doc//PeopleUpdateItem.md)
- [PeopleWithFacesResponseDto](doc//PeopleWithFacesResponseDto.md)
- [PersonCreateDto](doc//PersonCreateDto.md) - [PersonCreateDto](doc//PersonCreateDto.md)
- [PersonResponseDto](doc//PersonResponseDto.md) - [PersonResponseDto](doc//PersonResponseDto.md)
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
@@ -406,15 +402,12 @@ Class | Method | HTTP request | Description
- [UpdatePartnerDto](doc//UpdatePartnerDto.md) - [UpdatePartnerDto](doc//UpdatePartnerDto.md)
- [UpdateStackParentDto](doc//UpdateStackParentDto.md) - [UpdateStackParentDto](doc//UpdateStackParentDto.md)
- [UpdateTagDto](doc//UpdateTagDto.md) - [UpdateTagDto](doc//UpdateTagDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md) - [UsageByUserDto](doc//UsageByUserDto.md)
- [UserAdminCreateDto](doc//UserAdminCreateDto.md)
- [UserAdminDeleteDto](doc//UserAdminDeleteDto.md)
- [UserAdminResponseDto](doc//UserAdminResponseDto.md)
- [UserAdminUpdateDto](doc//UserAdminUpdateDto.md)
- [UserAvatarColor](doc//UserAvatarColor.md) - [UserAvatarColor](doc//UserAvatarColor.md)
- [UserDto](doc//UserDto.md)
- [UserResponseDto](doc//UserResponseDto.md) - [UserResponseDto](doc//UserResponseDto.md)
- [UserStatus](doc//UserStatus.md) - [UserStatus](doc//UserStatus.md)
- [UserUpdateMeDto](doc//UserUpdateMeDto.md)
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md) - [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
- [ValidateLibraryDto](doc//ValidateLibraryDto.md) - [ValidateLibraryDto](doc//ValidateLibraryDto.md)
- [ValidateLibraryImportPathResponseDto](doc//ValidateLibraryImportPathResponseDto.md) - [ValidateLibraryImportPathResponseDto](doc//ValidateLibraryImportPathResponseDto.md)
-16
View File
@@ -1,16 +0,0 @@
# openapi.model.PeopleWithFacesResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**numberOfFaces** | **int** | |
**visiblePeople** | [**List<PersonWithFacesResponseDto>**](PersonWithFacesResponseDto.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+4 -6
View File
@@ -112,6 +112,8 @@ part 'model/create_album_dto.dart';
part 'model/create_library_dto.dart'; part 'model/create_library_dto.dart';
part 'model/create_profile_image_response_dto.dart'; part 'model/create_profile_image_response_dto.dart';
part 'model/create_tag_dto.dart'; part 'model/create_tag_dto.dart';
part 'model/create_user_dto.dart';
part 'model/delete_user_dto.dart';
part 'model/download_archive_info.dart'; part 'model/download_archive_info.dart';
part 'model/download_info_dto.dart'; part 'model/download_info_dto.dart';
part 'model/download_response_dto.dart'; part 'model/download_response_dto.dart';
@@ -158,7 +160,6 @@ part 'model/path_type.dart';
part 'model/people_response_dto.dart'; part 'model/people_response_dto.dart';
part 'model/people_update_dto.dart'; part 'model/people_update_dto.dart';
part 'model/people_update_item.dart'; part 'model/people_update_item.dart';
part 'model/people_with_faces_response_dto.dart';
part 'model/person_create_dto.dart'; part 'model/person_create_dto.dart';
part 'model/person_response_dto.dart'; part 'model/person_response_dto.dart';
part 'model/person_statistics_response_dto.dart'; part 'model/person_statistics_response_dto.dart';
@@ -233,15 +234,12 @@ part 'model/update_library_dto.dart';
part 'model/update_partner_dto.dart'; part 'model/update_partner_dto.dart';
part 'model/update_stack_parent_dto.dart'; part 'model/update_stack_parent_dto.dart';
part 'model/update_tag_dto.dart'; part 'model/update_tag_dto.dart';
part 'model/update_user_dto.dart';
part 'model/usage_by_user_dto.dart'; part 'model/usage_by_user_dto.dart';
part 'model/user_admin_create_dto.dart';
part 'model/user_admin_delete_dto.dart';
part 'model/user_admin_response_dto.dart';
part 'model/user_admin_update_dto.dart';
part 'model/user_avatar_color.dart'; part 'model/user_avatar_color.dart';
part 'model/user_dto.dart';
part 'model/user_response_dto.dart'; part 'model/user_response_dto.dart';
part 'model/user_status.dart'; part 'model/user_status.dart';
part 'model/user_update_me_dto.dart';
part 'model/validate_access_token_response_dto.dart'; part 'model/validate_access_token_response_dto.dart';
part 'model/validate_library_dto.dart'; part 'model/validate_library_dto.dart';
part 'model/validate_library_import_path_response_dto.dart'; part 'model/validate_library_import_path_response_dto.dart';
+4 -4
View File
@@ -48,7 +48,7 @@ class AuthenticationApi {
/// Parameters: /// Parameters:
/// ///
/// * [ChangePasswordDto] changePasswordDto (required): /// * [ChangePasswordDto] changePasswordDto (required):
Future<UserAdminResponseDto?> changePassword(ChangePasswordDto changePasswordDto,) async { Future<UserResponseDto?> changePassword(ChangePasswordDto changePasswordDto,) async {
final response = await changePasswordWithHttpInfo(changePasswordDto,); final response = await changePasswordWithHttpInfo(changePasswordDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -57,7 +57,7 @@ class AuthenticationApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
} }
return null; return null;
@@ -183,7 +183,7 @@ class AuthenticationApi {
/// Parameters: /// Parameters:
/// ///
/// * [SignUpDto] signUpDto (required): /// * [SignUpDto] signUpDto (required):
Future<UserAdminResponseDto?> signUpAdmin(SignUpDto signUpDto,) async { Future<UserResponseDto?> signUpAdmin(SignUpDto signUpDto,) async {
final response = await signUpAdminWithHttpInfo(signUpDto,); final response = await signUpAdminWithHttpInfo(signUpDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -192,7 +192,7 @@ class AuthenticationApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
} }
return null; return null;
-48
View File
@@ -119,52 +119,4 @@ class FaceApi {
} }
return null; return null;
} }
/// Performs an HTTP 'DELETE /faces/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> unassignFaceWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/faces/{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<AssetFaceResponseDto?> unassignFace(String id,) async {
final response = await unassignFaceWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetFaceResponseDto',) as AssetFaceResponseDto;
}
return null;
}
} }
+4 -4
View File
@@ -95,7 +95,7 @@ class OAuthApi {
/// Parameters: /// Parameters:
/// ///
/// * [OAuthCallbackDto] oAuthCallbackDto (required): /// * [OAuthCallbackDto] oAuthCallbackDto (required):
Future<UserAdminResponseDto?> linkOAuthAccount(OAuthCallbackDto oAuthCallbackDto,) async { Future<UserResponseDto?> linkOAuthAccount(OAuthCallbackDto oAuthCallbackDto,) async {
final response = await linkOAuthAccountWithHttpInfo(oAuthCallbackDto,); final response = await linkOAuthAccountWithHttpInfo(oAuthCallbackDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -104,7 +104,7 @@ class OAuthApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
} }
return null; return null;
@@ -216,7 +216,7 @@ class OAuthApi {
); );
} }
Future<UserAdminResponseDto?> unlinkOAuthAccount() async { Future<UserResponseDto?> unlinkOAuthAccount() async {
final response = await unlinkOAuthAccountWithHttpInfo(); final response = await unlinkOAuthAccountWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -225,7 +225,7 @@ class OAuthApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
} }
return null; return null;
-50
View File
@@ -419,56 +419,6 @@ class PersonApi {
return null; return null;
} }
/// Performs an HTTP 'DELETE /people' operation and returns the [Response].
/// Parameters:
///
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
Future<Response> unassignFacesWithHttpInfo(AssetFaceUpdateDto assetFaceUpdateDto,) async {
// ignore: prefer_const_declarations
final path = r'/people';
// ignore: prefer_final_locals
Object? postBody = assetFaceUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
Future<List<BulkIdResponseDto>?> unassignFaces(AssetFaceUpdateDto assetFaceUpdateDto,) async {
final response = await unassignFacesWithHttpInfo(assetFaceUpdateDto,);
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<BulkIdResponseDto>') as List)
.cast<BulkIdResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'PUT /people' operation and returns the [Response]. /// Performs an HTTP 'PUT /people' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
+93 -239
View File
@@ -73,16 +73,16 @@ class UserApi {
return null; return null;
} }
/// Performs an HTTP 'POST /admin/users' operation and returns the [Response]. /// Performs an HTTP 'POST /users' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [UserAdminCreateDto] userAdminCreateDto (required): /// * [CreateUserDto] createUserDto (required):
Future<Response> createUserAdminWithHttpInfo(UserAdminCreateDto userAdminCreateDto,) async { Future<Response> createUserWithHttpInfo(CreateUserDto createUserDto,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/admin/users'; final path = r'/users';
// ignore: prefer_final_locals // ignore: prefer_final_locals
Object? postBody = userAdminCreateDto; Object? postBody = createUserDto;
final queryParams = <QueryParam>[]; final queryParams = <QueryParam>[];
final headerParams = <String, String>{}; final headerParams = <String, String>{};
@@ -104,9 +104,9 @@ class UserApi {
/// Parameters: /// Parameters:
/// ///
/// * [UserAdminCreateDto] userAdminCreateDto (required): /// * [CreateUserDto] createUserDto (required):
Future<UserAdminResponseDto?> createUserAdmin(UserAdminCreateDto userAdminCreateDto,) async { Future<UserResponseDto?> createUser(CreateUserDto createUserDto,) async {
final response = await createUserAdminWithHttpInfo(userAdminCreateDto,); final response = await createUserWithHttpInfo(createUserDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -114,7 +114,7 @@ class UserApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
} }
return null; return null;
@@ -153,19 +153,19 @@ class UserApi {
} }
} }
/// Performs an HTTP 'DELETE /admin/users/{id}' operation and returns the [Response]. /// Performs an HTTP 'DELETE /users/{id}' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [String] id (required): /// * [String] id (required):
/// ///
/// * [UserAdminDeleteDto] userAdminDeleteDto (required): /// * [DeleteUserDto] deleteUserDto (required):
Future<Response> deleteUserAdminWithHttpInfo(String id, UserAdminDeleteDto userAdminDeleteDto,) async { Future<Response> deleteUserWithHttpInfo(String id, DeleteUserDto deleteUserDto,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/admin/users/{id}' final path = r'/users/{id}'
.replaceAll('{id}', id); .replaceAll('{id}', id);
// ignore: prefer_final_locals // ignore: prefer_final_locals
Object? postBody = userAdminDeleteDto; Object? postBody = deleteUserDto;
final queryParams = <QueryParam>[]; final queryParams = <QueryParam>[];
final headerParams = <String, String>{}; final headerParams = <String, String>{};
@@ -189,9 +189,9 @@ class UserApi {
/// ///
/// * [String] id (required): /// * [String] id (required):
/// ///
/// * [UserAdminDeleteDto] userAdminDeleteDto (required): /// * [DeleteUserDto] deleteUserDto (required):
Future<UserAdminResponseDto?> deleteUserAdmin(String id, UserAdminDeleteDto userAdminDeleteDto,) async { Future<UserResponseDto?> deleteUser(String id, DeleteUserDto deleteUserDto,) async {
final response = await deleteUserAdminWithHttpInfo(id, userAdminDeleteDto,); final response = await deleteUserWithHttpInfo(id, deleteUserDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -199,14 +199,66 @@ class UserApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
} }
return null; return null;
} }
/// Performs an HTTP 'GET /users' operation and returns the [Response].
/// Parameters:
///
/// * [bool] isAll (required):
Future<Response> getAllUsersWithHttpInfo(bool isAll,) async {
// ignore: prefer_const_declarations
final path = r'/users';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'isAll', isAll));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [bool] isAll (required):
Future<List<UserResponseDto>?> getAllUsers(bool isAll,) async {
final response = await getAllUsersWithHttpInfo(isAll,);
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<UserResponseDto>') as List)
.cast<UserResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'GET /users/me' operation and returns the [Response]. /// Performs an HTTP 'GET /users/me' operation and returns the [Response].
Future<Response> getMyUserWithHttpInfo() async { Future<Response> getMyUserInfoWithHttpInfo() async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/users/me'; final path = r'/users/me';
@@ -231,8 +283,8 @@ class UserApi {
); );
} }
Future<UserAdminResponseDto?> getMyUser() async { Future<UserResponseDto?> getMyUserInfo() async {
final response = await getMyUserWithHttpInfo(); final response = await getMyUserInfoWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -240,7 +292,7 @@ class UserApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
} }
return null; return null;
@@ -298,7 +350,7 @@ class UserApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] id (required): /// * [String] id (required):
Future<Response> getUserWithHttpInfo(String id,) async { Future<Response> getUserByIdWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/users/{id}' final path = r'/users/{id}'
.replaceAll('{id}', id); .replaceAll('{id}', id);
@@ -327,8 +379,8 @@ class UserApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] id (required): /// * [String] id (required):
Future<UserResponseDto?> getUser(String id,) async { Future<UserResponseDto?> getUserById(String id,) async {
final response = await getUserWithHttpInfo(id,); final response = await getUserByIdWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -342,61 +394,13 @@ class UserApi {
return null; return null;
} }
/// Performs an HTTP 'GET /admin/users/{id}' operation and returns the [Response]. /// Performs an HTTP 'POST /users/{id}/restore' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [String] id (required): /// * [String] id (required):
Future<Response> getUserAdminWithHttpInfo(String id,) async { Future<Response> restoreUserWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/admin/users/{id}' final path = r'/users/{id}/restore'
.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,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<UserAdminResponseDto?> getUserAdmin(String id,) async {
final response = await getUserAdminWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto;
}
return null;
}
/// Performs an HTTP 'POST /admin/users/{id}/restore' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> restoreUserAdminWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/admin/users/{id}/restore'
.replaceAll('{id}', id); .replaceAll('{id}', id);
// ignore: prefer_final_locals // ignore: prefer_final_locals
@@ -423,8 +427,8 @@ class UserApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] id (required): /// * [String] id (required):
Future<UserAdminResponseDto?> restoreUserAdmin(String id,) async { Future<UserResponseDto?> restoreUser(String id,) async {
final response = await restoreUserAdminWithHttpInfo(id,); final response = await restoreUserWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -432,120 +436,22 @@ class UserApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
} }
return null; return null;
} }
/// Performs an HTTP 'GET /users' operation and returns the [Response]. /// Performs an HTTP 'PUT /users' operation and returns the [Response].
Future<Response> searchUsersWithHttpInfo() async { /// Parameters:
///
/// * [UpdateUserDto] updateUserDto (required):
Future<Response> updateUserWithHttpInfo(UpdateUserDto updateUserDto,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/users'; final path = r'/users';
// ignore: prefer_final_locals // ignore: prefer_final_locals
Object? postBody; Object? postBody = updateUserDto;
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<UserResponseDto>?> searchUsers() async {
final response = await searchUsersWithHttpInfo();
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<UserResponseDto>') as List)
.cast<UserResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'GET /admin/users' operation and returns the [Response].
/// Parameters:
///
/// * [bool] withDeleted:
Future<Response> searchUsersAdminWithHttpInfo({ bool? withDeleted, }) async {
// ignore: prefer_const_declarations
final path = r'/admin/users';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (withDeleted != null) {
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [bool] withDeleted:
Future<List<UserAdminResponseDto>?> searchUsersAdmin({ bool? withDeleted, }) async {
final response = await searchUsersAdminWithHttpInfo( withDeleted: withDeleted, );
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<UserAdminResponseDto>') as List)
.cast<UserAdminResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'PUT /users/me' operation and returns the [Response].
/// Parameters:
///
/// * [UserUpdateMeDto] userUpdateMeDto (required):
Future<Response> updateMyUserWithHttpInfo(UserUpdateMeDto userUpdateMeDto,) async {
// ignore: prefer_const_declarations
final path = r'/users/me';
// ignore: prefer_final_locals
Object? postBody = userUpdateMeDto;
final queryParams = <QueryParam>[]; final queryParams = <QueryParam>[];
final headerParams = <String, String>{}; final headerParams = <String, String>{};
@@ -567,9 +473,9 @@ class UserApi {
/// Parameters: /// Parameters:
/// ///
/// * [UserUpdateMeDto] userUpdateMeDto (required): /// * [UpdateUserDto] updateUserDto (required):
Future<UserAdminResponseDto?> updateMyUser(UserUpdateMeDto userUpdateMeDto,) async { Future<UserResponseDto?> updateUser(UpdateUserDto updateUserDto,) async {
final response = await updateMyUserWithHttpInfo(userUpdateMeDto,); final response = await updateUserWithHttpInfo(updateUserDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -577,59 +483,7 @@ class UserApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto;
}
return null;
}
/// Performs an HTTP 'PUT /admin/users/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [UserAdminUpdateDto] userAdminUpdateDto (required):
Future<Response> updateUserAdminWithHttpInfo(String id, UserAdminUpdateDto userAdminUpdateDto,) async {
// ignore: prefer_const_declarations
final path = r'/admin/users/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = userAdminUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [UserAdminUpdateDto] userAdminUpdateDto (required):
Future<UserAdminResponseDto?> updateUserAdmin(String id, UserAdminUpdateDto userAdminUpdateDto,) async {
final response = await updateUserAdminWithHttpInfo(id, userAdminUpdateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto;
} }
return null; return null;
+8 -12
View File
@@ -292,6 +292,10 @@ class ApiClient {
return CreateProfileImageResponseDto.fromJson(value); return CreateProfileImageResponseDto.fromJson(value);
case 'CreateTagDto': case 'CreateTagDto':
return CreateTagDto.fromJson(value); return CreateTagDto.fromJson(value);
case 'CreateUserDto':
return CreateUserDto.fromJson(value);
case 'DeleteUserDto':
return DeleteUserDto.fromJson(value);
case 'DownloadArchiveInfo': case 'DownloadArchiveInfo':
return DownloadArchiveInfo.fromJson(value); return DownloadArchiveInfo.fromJson(value);
case 'DownloadInfoDto': case 'DownloadInfoDto':
@@ -384,8 +388,6 @@ class ApiClient {
return PeopleUpdateDto.fromJson(value); return PeopleUpdateDto.fromJson(value);
case 'PeopleUpdateItem': case 'PeopleUpdateItem':
return PeopleUpdateItem.fromJson(value); return PeopleUpdateItem.fromJson(value);
case 'PeopleWithFacesResponseDto':
return PeopleWithFacesResponseDto.fromJson(value);
case 'PersonCreateDto': case 'PersonCreateDto':
return PersonCreateDto.fromJson(value); return PersonCreateDto.fromJson(value);
case 'PersonResponseDto': case 'PersonResponseDto':
@@ -534,24 +536,18 @@ class ApiClient {
return UpdateStackParentDto.fromJson(value); return UpdateStackParentDto.fromJson(value);
case 'UpdateTagDto': case 'UpdateTagDto':
return UpdateTagDto.fromJson(value); return UpdateTagDto.fromJson(value);
case 'UpdateUserDto':
return UpdateUserDto.fromJson(value);
case 'UsageByUserDto': case 'UsageByUserDto':
return UsageByUserDto.fromJson(value); return UsageByUserDto.fromJson(value);
case 'UserAdminCreateDto':
return UserAdminCreateDto.fromJson(value);
case 'UserAdminDeleteDto':
return UserAdminDeleteDto.fromJson(value);
case 'UserAdminResponseDto':
return UserAdminResponseDto.fromJson(value);
case 'UserAdminUpdateDto':
return UserAdminUpdateDto.fromJson(value);
case 'UserAvatarColor': case 'UserAvatarColor':
return UserAvatarColorTypeTransformer().decode(value); return UserAvatarColorTypeTransformer().decode(value);
case 'UserDto':
return UserDto.fromJson(value);
case 'UserResponseDto': case 'UserResponseDto':
return UserResponseDto.fromJson(value); return UserResponseDto.fromJson(value);
case 'UserStatus': case 'UserStatus':
return UserStatusTypeTransformer().decode(value); return UserStatusTypeTransformer().decode(value);
case 'UserUpdateMeDto':
return UserUpdateMeDto.fromJson(value);
case 'ValidateAccessTokenResponseDto': case 'ValidateAccessTokenResponseDto':
return ValidateAccessTokenResponseDto.fromJson(value); return ValidateAccessTokenResponseDto.fromJson(value);
case 'ValidateLibraryDto': case 'ValidateLibraryDto':
+2 -2
View File
@@ -31,7 +31,7 @@ class ActivityResponseDto {
ActivityResponseDtoTypeEnum type; ActivityResponseDtoTypeEnum type;
UserResponseDto user; UserDto user;
@override @override
bool operator ==(Object other) => identical(this, other) || other is ActivityResponseDto && bool operator ==(Object other) => identical(this, other) || other is ActivityResponseDto &&
@@ -87,7 +87,7 @@ class ActivityResponseDto {
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
type: ActivityResponseDtoTypeEnum.fromJson(json[r'type'])!, type: ActivityResponseDtoTypeEnum.fromJson(json[r'type'])!,
user: UserResponseDto.fromJson(json[r'user'])!, user: UserDto.fromJson(json[r'user'])!,
); );
} }
return null; return null;
+5 -15
View File
@@ -34,7 +34,7 @@ class AssetResponseDto {
required this.originalPath, required this.originalPath,
this.owner, this.owner,
required this.ownerId, required this.ownerId,
this.people, this.people = const [],
required this.resized, required this.resized,
this.smartInfo, this.smartInfo,
this.stack = const [], this.stack = const [],
@@ -102,13 +102,7 @@ class AssetResponseDto {
String ownerId; String ownerId;
/// List<PersonWithFacesResponseDto> people;
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
PeopleWithFacesResponseDto? people;
bool resized; bool resized;
@@ -157,7 +151,7 @@ class AssetResponseDto {
other.originalPath == originalPath && other.originalPath == originalPath &&
other.owner == owner && other.owner == owner &&
other.ownerId == ownerId && other.ownerId == ownerId &&
other.people == people && _deepEquality.equals(other.people, people) &&
other.resized == resized && other.resized == resized &&
other.smartInfo == smartInfo && other.smartInfo == smartInfo &&
_deepEquality.equals(other.stack, stack) && _deepEquality.equals(other.stack, stack) &&
@@ -192,7 +186,7 @@ class AssetResponseDto {
(originalPath.hashCode) + (originalPath.hashCode) +
(owner == null ? 0 : owner!.hashCode) + (owner == null ? 0 : owner!.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(people == null ? 0 : people!.hashCode) + (people.hashCode) +
(resized.hashCode) + (resized.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) + (smartInfo == null ? 0 : smartInfo!.hashCode) +
(stack.hashCode) + (stack.hashCode) +
@@ -249,11 +243,7 @@ class AssetResponseDto {
// json[r'owner'] = null; // json[r'owner'] = null;
} }
json[r'ownerId'] = this.ownerId; json[r'ownerId'] = this.ownerId;
if (this.people != null) {
json[r'people'] = this.people; json[r'people'] = this.people;
} else {
// json[r'people'] = null;
}
json[r'resized'] = this.resized; json[r'resized'] = this.resized;
if (this.smartInfo != null) { if (this.smartInfo != null) {
json[r'smartInfo'] = this.smartInfo; json[r'smartInfo'] = this.smartInfo;
@@ -311,7 +301,7 @@ class AssetResponseDto {
originalPath: mapValueOfType<String>(json, r'originalPath')!, originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']), owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
people: PeopleWithFacesResponseDto.fromJson(json[r'people']), people: PersonWithFacesResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized')!, resized: mapValueOfType<bool>(json, r'resized')!,
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']), smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
stack: AssetResponseDto.listFromJson(json[r'stack']), stack: AssetResponseDto.listFromJson(json[r'stack']),
@@ -10,9 +10,9 @@
part of openapi.api; part of openapi.api;
class UserAdminCreateDto { class CreateUserDto {
/// Returns a new [UserAdminCreateDto] instance. /// Returns a new [CreateUserDto] instance.
UserAdminCreateDto({ CreateUserDto({
required this.email, required this.email,
this.memoriesEnabled, this.memoriesEnabled,
required this.name, required this.name,
@@ -59,7 +59,7 @@ class UserAdminCreateDto {
String? storageLabel; String? storageLabel;
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto && bool operator ==(Object other) => identical(this, other) || other is CreateUserDto &&
other.email == email && other.email == email &&
other.memoriesEnabled == memoriesEnabled && other.memoriesEnabled == memoriesEnabled &&
other.name == name && other.name == name &&
@@ -82,7 +82,7 @@ class UserAdminCreateDto {
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @override
String toString() => 'UserAdminCreateDto[email=$email, memoriesEnabled=$memoriesEnabled, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; String toString() => 'CreateUserDto[email=$email, memoriesEnabled=$memoriesEnabled, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -117,14 +117,14 @@ class UserAdminCreateDto {
return json; return json;
} }
/// Returns a new [UserAdminCreateDto] instance and imports its values from /// Returns a new [CreateUserDto] 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 UserAdminCreateDto? fromJson(dynamic value) { static CreateUserDto? fromJson(dynamic value) {
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserAdminCreateDto( return CreateUserDto(
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'), memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
@@ -138,11 +138,11 @@ class UserAdminCreateDto {
return null; return null;
} }
static List<UserAdminCreateDto> listFromJson(dynamic json, {bool growable = false,}) { static List<CreateUserDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserAdminCreateDto>[]; final result = <CreateUserDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = UserAdminCreateDto.fromJson(row); final value = CreateUserDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -151,12 +151,12 @@ class UserAdminCreateDto {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, UserAdminCreateDto> mapFromJson(dynamic json) { static Map<String, CreateUserDto> mapFromJson(dynamic json) {
final map = <String, UserAdminCreateDto>{}; final map = <String, CreateUserDto>{};
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 = UserAdminCreateDto.fromJson(entry.value); final value = CreateUserDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -165,14 +165,14 @@ class UserAdminCreateDto {
return map; return map;
} }
// maps a json object with a list of UserAdminCreateDto-objects as value to a dart map // maps a json object with a list of CreateUserDto-objects as value to a dart map
static Map<String, List<UserAdminCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<CreateUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserAdminCreateDto>>{}; final map = <String, List<CreateUserDto>>{};
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] = UserAdminCreateDto.listFromJson(entry.value, growable: growable,); map[entry.key] = CreateUserDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;
@@ -10,9 +10,9 @@
part of openapi.api; part of openapi.api;
class UserAdminDeleteDto { class DeleteUserDto {
/// Returns a new [UserAdminDeleteDto] instance. /// Returns a new [DeleteUserDto] instance.
UserAdminDeleteDto({ DeleteUserDto({
this.force, this.force,
}); });
@@ -25,7 +25,7 @@ class UserAdminDeleteDto {
bool? force; bool? force;
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserAdminDeleteDto && bool operator ==(Object other) => identical(this, other) || other is DeleteUserDto &&
other.force == force; other.force == force;
@override @override
@@ -34,7 +34,7 @@ class UserAdminDeleteDto {
(force == null ? 0 : force!.hashCode); (force == null ? 0 : force!.hashCode);
@override @override
String toString() => 'UserAdminDeleteDto[force=$force]'; String toString() => 'DeleteUserDto[force=$force]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -46,25 +46,25 @@ class UserAdminDeleteDto {
return json; return json;
} }
/// Returns a new [UserAdminDeleteDto] instance and imports its values from /// Returns a new [DeleteUserDto] 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 UserAdminDeleteDto? fromJson(dynamic value) { static DeleteUserDto? fromJson(dynamic value) {
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserAdminDeleteDto( return DeleteUserDto(
force: mapValueOfType<bool>(json, r'force'), force: mapValueOfType<bool>(json, r'force'),
); );
} }
return null; return null;
} }
static List<UserAdminDeleteDto> listFromJson(dynamic json, {bool growable = false,}) { static List<DeleteUserDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserAdminDeleteDto>[]; final result = <DeleteUserDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = UserAdminDeleteDto.fromJson(row); final value = DeleteUserDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -73,12 +73,12 @@ class UserAdminDeleteDto {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, UserAdminDeleteDto> mapFromJson(dynamic json) { static Map<String, DeleteUserDto> mapFromJson(dynamic json) {
final map = <String, UserAdminDeleteDto>{}; final map = <String, DeleteUserDto>{};
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 = UserAdminDeleteDto.fromJson(entry.value); final value = DeleteUserDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -87,14 +87,14 @@ class UserAdminDeleteDto {
return map; return map;
} }
// maps a json object with a list of UserAdminDeleteDto-objects as value to a dart map // maps a json object with a list of DeleteUserDto-objects as value to a dart map
static Map<String, List<UserAdminDeleteDto>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<DeleteUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserAdminDeleteDto>>{}; final map = <String, List<DeleteUserDto>>{};
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] = UserAdminDeleteDto.listFromJson(entry.value, growable: growable,); map[entry.key] = DeleteUserDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;
+116 -3
View File
@@ -14,15 +14,30 @@ class PartnerResponseDto {
/// Returns a new [PartnerResponseDto] instance. /// Returns a new [PartnerResponseDto] instance.
PartnerResponseDto({ PartnerResponseDto({
required this.avatarColor, required this.avatarColor,
required this.createdAt,
required this.deletedAt,
required this.email, required this.email,
required this.id, required this.id,
this.inTimeline, this.inTimeline,
required this.isAdmin,
this.memoriesEnabled,
required this.name, required this.name,
required this.oauthId,
required this.profileImagePath, required this.profileImagePath,
required this.quotaSizeInBytes,
required this.quotaUsageInBytes,
required this.shouldChangePassword,
required this.status,
required this.storageLabel,
required this.updatedAt,
}); });
UserAvatarColor avatarColor; UserAvatarColor avatarColor;
DateTime createdAt;
DateTime? deletedAt;
String email; String email;
String id; String id;
@@ -35,44 +50,121 @@ class PartnerResponseDto {
/// ///
bool? inTimeline; bool? inTimeline;
bool isAdmin;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? memoriesEnabled;
String name; String name;
String oauthId;
String profileImagePath; String profileImagePath;
int? quotaSizeInBytes;
int? quotaUsageInBytes;
bool shouldChangePassword;
UserStatus status;
String? storageLabel;
DateTime updatedAt;
@override @override
bool operator ==(Object other) => identical(this, other) || other is PartnerResponseDto && bool operator ==(Object other) => identical(this, other) || other is PartnerResponseDto &&
other.avatarColor == avatarColor && other.avatarColor == avatarColor &&
other.createdAt == createdAt &&
other.deletedAt == deletedAt &&
other.email == email && other.email == email &&
other.id == id && other.id == id &&
other.inTimeline == inTimeline && other.inTimeline == inTimeline &&
other.isAdmin == isAdmin &&
other.memoriesEnabled == memoriesEnabled &&
other.name == name && other.name == name &&
other.profileImagePath == profileImagePath; other.oauthId == oauthId &&
other.profileImagePath == profileImagePath &&
other.quotaSizeInBytes == quotaSizeInBytes &&
other.quotaUsageInBytes == quotaUsageInBytes &&
other.shouldChangePassword == shouldChangePassword &&
other.status == status &&
other.storageLabel == storageLabel &&
other.updatedAt == updatedAt;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor.hashCode) + (avatarColor.hashCode) +
(createdAt.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) + (email.hashCode) +
(id.hashCode) + (id.hashCode) +
(inTimeline == null ? 0 : inTimeline!.hashCode) + (inTimeline == null ? 0 : inTimeline!.hashCode) +
(isAdmin.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
(name.hashCode) + (name.hashCode) +
(profileImagePath.hashCode); (oauthId.hashCode) +
(profileImagePath.hashCode) +
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
(quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) +
(shouldChangePassword.hashCode) +
(status.hashCode) +
(storageLabel == null ? 0 : storageLabel!.hashCode) +
(updatedAt.hashCode);
@override @override
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, email=$email, id=$id, inTimeline=$inTimeline, name=$name, profileImagePath=$profileImagePath]'; String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'avatarColor'] = this.avatarColor; json[r'avatarColor'] = this.avatarColor;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
if (this.deletedAt != null) {
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
} else {
// json[r'deletedAt'] = null;
}
json[r'email'] = this.email; json[r'email'] = this.email;
json[r'id'] = this.id; json[r'id'] = this.id;
if (this.inTimeline != null) { if (this.inTimeline != null) {
json[r'inTimeline'] = this.inTimeline; json[r'inTimeline'] = this.inTimeline;
} else { } else {
// json[r'inTimeline'] = null; // json[r'inTimeline'] = null;
}
json[r'isAdmin'] = this.isAdmin;
if (this.memoriesEnabled != null) {
json[r'memoriesEnabled'] = this.memoriesEnabled;
} else {
// json[r'memoriesEnabled'] = null;
} }
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'oauthId'] = this.oauthId;
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
if (this.quotaSizeInBytes != null) {
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
} else {
// json[r'quotaSizeInBytes'] = null;
}
if (this.quotaUsageInBytes != null) {
json[r'quotaUsageInBytes'] = this.quotaUsageInBytes;
} else {
// json[r'quotaUsageInBytes'] = null;
}
json[r'shouldChangePassword'] = this.shouldChangePassword;
json[r'status'] = this.status;
if (this.storageLabel != null) {
json[r'storageLabel'] = this.storageLabel;
} else {
// json[r'storageLabel'] = null;
}
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json; return json;
} }
@@ -85,11 +177,22 @@ class PartnerResponseDto {
return PartnerResponseDto( return PartnerResponseDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
createdAt: mapDateTime(json, r'createdAt', r'')!,
deletedAt: mapDateTime(json, r'deletedAt', r''),
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
inTimeline: mapValueOfType<bool>(json, r'inTimeline'), inTimeline: mapValueOfType<bool>(json, r'inTimeline'),
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
oauthId: mapValueOfType<String>(json, r'oauthId')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
status: UserStatus.fromJson(json[r'status'])!,
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
); );
} }
return null; return null;
@@ -138,10 +241,20 @@ class PartnerResponseDto {
/// 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>{
'avatarColor', 'avatarColor',
'createdAt',
'deletedAt',
'email', 'email',
'id', 'id',
'isAdmin',
'name', 'name',
'oauthId',
'profileImagePath', 'profileImagePath',
'quotaSizeInBytes',
'quotaUsageInBytes',
'shouldChangePassword',
'status',
'storageLabel',
'updatedAt',
}; };
} }
@@ -1,106 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PeopleWithFacesResponseDto {
/// Returns a new [PeopleWithFacesResponseDto] instance.
PeopleWithFacesResponseDto({
required this.numberOfFaces,
this.visiblePeople = const [],
});
int numberOfFaces;
List<PersonWithFacesResponseDto> visiblePeople;
@override
bool operator ==(Object other) => identical(this, other) || other is PeopleWithFacesResponseDto &&
other.numberOfFaces == numberOfFaces &&
_deepEquality.equals(other.visiblePeople, visiblePeople);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(numberOfFaces.hashCode) +
(visiblePeople.hashCode);
@override
String toString() => 'PeopleWithFacesResponseDto[numberOfFaces=$numberOfFaces, visiblePeople=$visiblePeople]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'numberOfFaces'] = this.numberOfFaces;
json[r'visiblePeople'] = this.visiblePeople;
return json;
}
/// Returns a new [PeopleWithFacesResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PeopleWithFacesResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return PeopleWithFacesResponseDto(
numberOfFaces: mapValueOfType<int>(json, r'numberOfFaces')!,
visiblePeople: PersonWithFacesResponseDto.listFromJson(json[r'visiblePeople']),
);
}
return null;
}
static List<PeopleWithFacesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PeopleWithFacesResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PeopleWithFacesResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PeopleWithFacesResponseDto> mapFromJson(dynamic json) {
final map = <String, PeopleWithFacesResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PeopleWithFacesResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PeopleWithFacesResponseDto-objects as value to a dart map
static Map<String, List<PeopleWithFacesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PeopleWithFacesResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PeopleWithFacesResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'numberOfFaces',
'visiblePeople',
};
}
@@ -10,11 +10,13 @@
part of openapi.api; part of openapi.api;
class UserAdminUpdateDto { class UpdateUserDto {
/// Returns a new [UserAdminUpdateDto] instance. /// Returns a new [UpdateUserDto] instance.
UserAdminUpdateDto({ UpdateUserDto({
this.avatarColor, this.avatarColor,
this.email, this.email,
required this.id,
this.isAdmin,
this.memoriesEnabled, this.memoriesEnabled,
this.name, this.name,
this.password, this.password,
@@ -39,6 +41,16 @@ class UserAdminUpdateDto {
/// ///
String? email; String? email;
String id;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? isAdmin;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
@@ -74,12 +86,20 @@ class UserAdminUpdateDto {
/// ///
bool? shouldChangePassword; bool? shouldChangePassword;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? storageLabel; String? storageLabel;
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto && bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
other.avatarColor == avatarColor && other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.id == id &&
other.isAdmin == isAdmin &&
other.memoriesEnabled == memoriesEnabled && other.memoriesEnabled == memoriesEnabled &&
other.name == name && other.name == name &&
other.password == password && other.password == password &&
@@ -92,6 +112,8 @@ class UserAdminUpdateDto {
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) + (avatarColor == null ? 0 : avatarColor!.hashCode) +
(email == null ? 0 : email!.hashCode) + (email == null ? 0 : email!.hashCode) +
(id.hashCode) +
(isAdmin == null ? 0 : isAdmin!.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
(name == null ? 0 : name!.hashCode) + (name == null ? 0 : name!.hashCode) +
(password == null ? 0 : password!.hashCode) + (password == null ? 0 : password!.hashCode) +
@@ -100,7 +122,7 @@ class UserAdminUpdateDto {
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @override
String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -113,6 +135,12 @@ class UserAdminUpdateDto {
json[r'email'] = this.email; json[r'email'] = this.email;
} else { } else {
// json[r'email'] = null; // json[r'email'] = null;
}
json[r'id'] = this.id;
if (this.isAdmin != null) {
json[r'isAdmin'] = this.isAdmin;
} else {
// json[r'isAdmin'] = null;
} }
if (this.memoriesEnabled != null) { if (this.memoriesEnabled != null) {
json[r'memoriesEnabled'] = this.memoriesEnabled; json[r'memoriesEnabled'] = this.memoriesEnabled;
@@ -147,16 +175,18 @@ class UserAdminUpdateDto {
return json; return json;
} }
/// Returns a new [UserAdminUpdateDto] instance and imports its values from /// Returns a new [UpdateUserDto] 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 UserAdminUpdateDto? fromJson(dynamic value) { static UpdateUserDto? fromJson(dynamic value) {
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserAdminUpdateDto( return UpdateUserDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'), email: mapValueOfType<String>(json, r'email'),
id: mapValueOfType<String>(json, r'id')!,
isAdmin: mapValueOfType<bool>(json, r'isAdmin'),
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'), memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
password: mapValueOfType<String>(json, r'password'), password: mapValueOfType<String>(json, r'password'),
@@ -168,11 +198,11 @@ class UserAdminUpdateDto {
return null; return null;
} }
static List<UserAdminUpdateDto> listFromJson(dynamic json, {bool growable = false,}) { static List<UpdateUserDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserAdminUpdateDto>[]; final result = <UpdateUserDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = UserAdminUpdateDto.fromJson(row); final value = UpdateUserDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -181,12 +211,12 @@ class UserAdminUpdateDto {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, UserAdminUpdateDto> mapFromJson(dynamic json) { static Map<String, UpdateUserDto> mapFromJson(dynamic json) {
final map = <String, UserAdminUpdateDto>{}; final map = <String, UpdateUserDto>{};
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 = UserAdminUpdateDto.fromJson(entry.value); final value = UpdateUserDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -195,14 +225,14 @@ class UserAdminUpdateDto {
return map; return map;
} }
// maps a json object with a list of UserAdminUpdateDto-objects as value to a dart map // maps a json object with a list of UpdateUserDto-objects as value to a dart map
static Map<String, List<UserAdminUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<UpdateUserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserAdminUpdateDto>>{}; final map = <String, List<UpdateUserDto>>{};
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] = UserAdminUpdateDto.listFromJson(entry.value, growable: growable,); map[entry.key] = UpdateUserDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;
@@ -210,6 +240,7 @@ class UserAdminUpdateDto {
/// 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>{
'id',
}; };
} }
-243
View File
@@ -1,243 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 UserAdminResponseDto {
/// Returns a new [UserAdminResponseDto] instance.
UserAdminResponseDto({
required this.avatarColor,
required this.createdAt,
required this.deletedAt,
required this.email,
required this.id,
required this.isAdmin,
this.memoriesEnabled,
required this.name,
required this.oauthId,
required this.profileImagePath,
required this.quotaSizeInBytes,
required this.quotaUsageInBytes,
required this.shouldChangePassword,
required this.status,
required this.storageLabel,
required this.updatedAt,
});
UserAvatarColor avatarColor;
DateTime createdAt;
DateTime? deletedAt;
String email;
String id;
bool isAdmin;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? memoriesEnabled;
String name;
String oauthId;
String profileImagePath;
int? quotaSizeInBytes;
int? quotaUsageInBytes;
bool shouldChangePassword;
UserStatus status;
String? storageLabel;
DateTime updatedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is UserAdminResponseDto &&
other.avatarColor == avatarColor &&
other.createdAt == createdAt &&
other.deletedAt == deletedAt &&
other.email == email &&
other.id == id &&
other.isAdmin == isAdmin &&
other.memoriesEnabled == memoriesEnabled &&
other.name == name &&
other.oauthId == oauthId &&
other.profileImagePath == profileImagePath &&
other.quotaSizeInBytes == quotaSizeInBytes &&
other.quotaUsageInBytes == quotaUsageInBytes &&
other.shouldChangePassword == shouldChangePassword &&
other.status == status &&
other.storageLabel == storageLabel &&
other.updatedAt == updatedAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(avatarColor.hashCode) +
(createdAt.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) +
(id.hashCode) +
(isAdmin.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
(name.hashCode) +
(oauthId.hashCode) +
(profileImagePath.hashCode) +
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
(quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) +
(shouldChangePassword.hashCode) +
(status.hashCode) +
(storageLabel == null ? 0 : storageLabel!.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'avatarColor'] = this.avatarColor;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
if (this.deletedAt != null) {
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
} else {
// json[r'deletedAt'] = null;
}
json[r'email'] = this.email;
json[r'id'] = this.id;
json[r'isAdmin'] = this.isAdmin;
if (this.memoriesEnabled != null) {
json[r'memoriesEnabled'] = this.memoriesEnabled;
} else {
// json[r'memoriesEnabled'] = null;
}
json[r'name'] = this.name;
json[r'oauthId'] = this.oauthId;
json[r'profileImagePath'] = this.profileImagePath;
if (this.quotaSizeInBytes != null) {
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
} else {
// json[r'quotaSizeInBytes'] = null;
}
if (this.quotaUsageInBytes != null) {
json[r'quotaUsageInBytes'] = this.quotaUsageInBytes;
} else {
// json[r'quotaUsageInBytes'] = null;
}
json[r'shouldChangePassword'] = this.shouldChangePassword;
json[r'status'] = this.status;
if (this.storageLabel != null) {
json[r'storageLabel'] = this.storageLabel;
} else {
// json[r'storageLabel'] = null;
}
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json;
}
/// Returns a new [UserAdminResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UserAdminResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return UserAdminResponseDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
createdAt: mapDateTime(json, r'createdAt', r'')!,
deletedAt: mapDateTime(json, r'deletedAt', r''),
email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!,
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name')!,
oauthId: mapValueOfType<String>(json, r'oauthId')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
status: UserStatus.fromJson(json[r'status'])!,
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
);
}
return null;
}
static List<UserAdminResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserAdminResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UserAdminResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UserAdminResponseDto> mapFromJson(dynamic json) {
final map = <String, UserAdminResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UserAdminResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UserAdminResponseDto-objects as value to a dart map
static Map<String, List<UserAdminResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserAdminResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = UserAdminResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'avatarColor',
'createdAt',
'deletedAt',
'email',
'id',
'isAdmin',
'name',
'oauthId',
'profileImagePath',
'quotaSizeInBytes',
'quotaUsageInBytes',
'shouldChangePassword',
'status',
'storageLabel',
'updatedAt',
};
}
+130
View File
@@ -0,0 +1,130 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 UserDto {
/// Returns a new [UserDto] instance.
UserDto({
required this.avatarColor,
required this.email,
required this.id,
required this.name,
required this.profileImagePath,
});
UserAvatarColor avatarColor;
String email;
String id;
String name;
String profileImagePath;
@override
bool operator ==(Object other) => identical(this, other) || other is UserDto &&
other.avatarColor == avatarColor &&
other.email == email &&
other.id == id &&
other.name == name &&
other.profileImagePath == profileImagePath;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(avatarColor.hashCode) +
(email.hashCode) +
(id.hashCode) +
(name.hashCode) +
(profileImagePath.hashCode);
@override
String toString() => 'UserDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'avatarColor'] = this.avatarColor;
json[r'email'] = this.email;
json[r'id'] = this.id;
json[r'name'] = this.name;
json[r'profileImagePath'] = this.profileImagePath;
return json;
}
/// Returns a new [UserDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UserDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return UserDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!,
name: mapValueOfType<String>(json, r'name')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
);
}
return null;
}
static List<UserDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UserDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UserDto> mapFromJson(dynamic json) {
final map = <String, UserDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UserDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UserDto-objects as value to a dart map
static Map<String, List<UserDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = UserDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'avatarColor',
'email',
'id',
'name',
'profileImagePath',
};
}
+116 -3
View File
@@ -14,49 +14,141 @@ class UserResponseDto {
/// Returns a new [UserResponseDto] instance. /// Returns a new [UserResponseDto] instance.
UserResponseDto({ UserResponseDto({
required this.avatarColor, required this.avatarColor,
required this.createdAt,
required this.deletedAt,
required this.email, required this.email,
required this.id, required this.id,
required this.isAdmin,
this.memoriesEnabled,
required this.name, required this.name,
required this.oauthId,
required this.profileImagePath, required this.profileImagePath,
required this.quotaSizeInBytes,
required this.quotaUsageInBytes,
required this.shouldChangePassword,
required this.status,
required this.storageLabel,
required this.updatedAt,
}); });
UserAvatarColor avatarColor; UserAvatarColor avatarColor;
DateTime createdAt;
DateTime? deletedAt;
String email; String email;
String id; String id;
bool isAdmin;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? memoriesEnabled;
String name; String name;
String oauthId;
String profileImagePath; String profileImagePath;
int? quotaSizeInBytes;
int? quotaUsageInBytes;
bool shouldChangePassword;
UserStatus status;
String? storageLabel;
DateTime updatedAt;
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserResponseDto && bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
other.avatarColor == avatarColor && other.avatarColor == avatarColor &&
other.createdAt == createdAt &&
other.deletedAt == deletedAt &&
other.email == email && other.email == email &&
other.id == id && other.id == id &&
other.isAdmin == isAdmin &&
other.memoriesEnabled == memoriesEnabled &&
other.name == name && other.name == name &&
other.profileImagePath == profileImagePath; other.oauthId == oauthId &&
other.profileImagePath == profileImagePath &&
other.quotaSizeInBytes == quotaSizeInBytes &&
other.quotaUsageInBytes == quotaUsageInBytes &&
other.shouldChangePassword == shouldChangePassword &&
other.status == status &&
other.storageLabel == storageLabel &&
other.updatedAt == updatedAt;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor.hashCode) + (avatarColor.hashCode) +
(createdAt.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(email.hashCode) + (email.hashCode) +
(id.hashCode) + (id.hashCode) +
(isAdmin.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
(name.hashCode) + (name.hashCode) +
(profileImagePath.hashCode); (oauthId.hashCode) +
(profileImagePath.hashCode) +
(quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) +
(quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) +
(shouldChangePassword.hashCode) +
(status.hashCode) +
(storageLabel == null ? 0 : storageLabel!.hashCode) +
(updatedAt.hashCode);
@override @override
String toString() => 'UserResponseDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]'; String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'avatarColor'] = this.avatarColor; json[r'avatarColor'] = this.avatarColor;
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
if (this.deletedAt != null) {
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
} else {
// json[r'deletedAt'] = null;
}
json[r'email'] = this.email; json[r'email'] = this.email;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isAdmin'] = this.isAdmin;
if (this.memoriesEnabled != null) {
json[r'memoriesEnabled'] = this.memoriesEnabled;
} else {
// json[r'memoriesEnabled'] = null;
}
json[r'name'] = this.name; json[r'name'] = this.name;
json[r'oauthId'] = this.oauthId;
json[r'profileImagePath'] = this.profileImagePath; json[r'profileImagePath'] = this.profileImagePath;
if (this.quotaSizeInBytes != null) {
json[r'quotaSizeInBytes'] = this.quotaSizeInBytes;
} else {
// json[r'quotaSizeInBytes'] = null;
}
if (this.quotaUsageInBytes != null) {
json[r'quotaUsageInBytes'] = this.quotaUsageInBytes;
} else {
// json[r'quotaUsageInBytes'] = null;
}
json[r'shouldChangePassword'] = this.shouldChangePassword;
json[r'status'] = this.status;
if (this.storageLabel != null) {
json[r'storageLabel'] = this.storageLabel;
} else {
// json[r'storageLabel'] = null;
}
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json; return json;
} }
@@ -69,10 +161,21 @@ class UserResponseDto {
return UserResponseDto( return UserResponseDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
createdAt: mapDateTime(json, r'createdAt', r'')!,
deletedAt: mapDateTime(json, r'deletedAt', r''),
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
oauthId: mapValueOfType<String>(json, r'oauthId')!,
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!, profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
status: UserStatus.fromJson(json[r'status'])!,
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
); );
} }
return null; return null;
@@ -121,10 +224,20 @@ class UserResponseDto {
/// 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>{
'avatarColor', 'avatarColor',
'createdAt',
'deletedAt',
'email', 'email',
'id', 'id',
'isAdmin',
'name', 'name',
'oauthId',
'profileImagePath', 'profileImagePath',
'quotaSizeInBytes',
'quotaUsageInBytes',
'shouldChangePassword',
'status',
'storageLabel',
'updatedAt',
}; };
} }
-175
View File
@@ -1,175 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 UserUpdateMeDto {
/// Returns a new [UserUpdateMeDto] instance.
UserUpdateMeDto({
this.avatarColor,
this.email,
this.memoriesEnabled,
this.name,
this.password,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
UserAvatarColor? avatarColor;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? email;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? memoriesEnabled;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? name;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? password;
@override
bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto &&
other.avatarColor == avatarColor &&
other.email == email &&
other.memoriesEnabled == memoriesEnabled &&
other.name == name &&
other.password == password;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email == null ? 0 : email!.hashCode) +
(memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) +
(name == null ? 0 : name!.hashCode) +
(password == null ? 0 : password!.hashCode);
@override
String toString() => 'UserUpdateMeDto[avatarColor=$avatarColor, email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
if (this.email != null) {
json[r'email'] = this.email;
} else {
// json[r'email'] = null;
}
if (this.memoriesEnabled != null) {
json[r'memoriesEnabled'] = this.memoriesEnabled;
} else {
// json[r'memoriesEnabled'] = null;
}
if (this.name != null) {
json[r'name'] = this.name;
} else {
// json[r'name'] = null;
}
if (this.password != null) {
json[r'password'] = this.password;
} else {
// json[r'password'] = null;
}
return json;
}
/// Returns a new [UserUpdateMeDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UserUpdateMeDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return UserUpdateMeDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'),
memoriesEnabled: mapValueOfType<bool>(json, r'memoriesEnabled'),
name: mapValueOfType<String>(json, r'name'),
password: mapValueOfType<String>(json, r'password'),
);
}
return null;
}
static List<UserUpdateMeDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UserUpdateMeDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UserUpdateMeDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UserUpdateMeDto> mapFromJson(dynamic json) {
final map = <String, UserUpdateMeDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UserUpdateMeDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UserUpdateMeDto-objects as value to a dart map
static Map<String, List<UserUpdateMeDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UserUpdateMeDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = UserUpdateMeDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
@@ -1,32 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PeopleWithFacesResponseDto
void main() {
// final instance = PeopleWithFacesResponseDto();
group('test PeopleWithFacesResponseDto', () {
// int numberOfFaces
test('to test the property `numberOfFaces`', () async {
// TODO
});
// List<PersonWithFacesResponseDto> visiblePeople (default value: const [])
test('to test the property `visiblePeople`', () async {
// TODO
});
});
}
File diff suppressed because it is too large Load Diff
-9
View File
@@ -13,22 +13,13 @@ npm i --save @immich/sdk
For a more detailed example, check out the [`@immich/cli`](https://github.com/immich-app/immich/tree/main/cli). For a more detailed example, check out the [`@immich/cli`](https://github.com/immich-app/immich/tree/main/cli).
```typescript ```typescript
<<<<<<< HEAD
import { getAllAlbums, getAllAssets, getMyUser, init } from "@immich/sdk";
=======
import { getAllAlbums, getMyUserInfo, init } from "@immich/sdk"; import { getAllAlbums, getMyUserInfo, init } from "@immich/sdk";
>>>>>>> e7c8501930a988dfb6c23ce1c48b0beb076a58c2
const API_KEY = "<API_KEY>"; // process.env.IMMICH_API_KEY const API_KEY = "<API_KEY>"; // process.env.IMMICH_API_KEY
init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY }); init({ baseUrl: "https://demo.immich.app/api", apiKey: API_KEY });
<<<<<<< HEAD
const user = await getMyUser();
const assets = await getAllAssets({ take: 1000 });
=======
const user = await getMyUserInfo(); const user = await getMyUserInfo();
>>>>>>> e7c8501930a988dfb6c23ce1c48b0beb076a58c2
const albums = await getAllAlbums({}); const albums = await getAllAlbums({});
console.log({ user, albums }); console.log({ user, albums });
+111 -163
View File
@@ -14,7 +14,7 @@ const oazapfts = Oazapfts.runtime(defaults);
export const servers = { export const servers = {
server1: "/api" server1: "/api"
}; };
export type UserResponseDto = { export type UserDto = {
avatarColor: UserAvatarColor; avatarColor: UserAvatarColor;
email: string; email: string;
id: string; id: string;
@@ -27,7 +27,7 @@ export type ActivityResponseDto = {
createdAt: string; createdAt: string;
id: string; id: string;
"type": Type; "type": Type;
user: UserResponseDto; user: UserDto;
}; };
export type ActivityCreateDto = { export type ActivityCreateDto = {
albumId: string; albumId: string;
@@ -38,7 +38,7 @@ export type ActivityCreateDto = {
export type ActivityStatisticsResponseDto = { export type ActivityStatisticsResponseDto = {
comments: number; comments: number;
}; };
export type UserAdminResponseDto = { export type UserResponseDto = {
avatarColor: UserAvatarColor; avatarColor: UserAvatarColor;
createdAt: string; createdAt: string;
deletedAt: string | null; deletedAt: string | null;
@@ -56,29 +56,6 @@ export type UserAdminResponseDto = {
storageLabel: string | null; storageLabel: string | null;
updatedAt: string; updatedAt: string;
}; };
export type UserAdminCreateDto = {
email: string;
memoriesEnabled?: boolean;
name: string;
notify?: boolean;
password: string;
quotaSizeInBytes?: number | null;
shouldChangePassword?: boolean;
storageLabel?: string | null;
};
export type UserAdminDeleteDto = {
force?: boolean;
};
export type UserAdminUpdateDto = {
avatarColor?: UserAvatarColor;
email?: string;
memoriesEnabled?: boolean;
name?: string;
password?: string;
quotaSizeInBytes?: number | null;
shouldChangePassword?: boolean;
storageLabel?: string | null;
};
export type AlbumUserResponseDto = { export type AlbumUserResponseDto = {
role: AlbumUserRole; role: AlbumUserRole;
user: UserResponseDto; user: UserResponseDto;
@@ -123,10 +100,6 @@ export type PersonWithFacesResponseDto = {
name: string; name: string;
thumbnailPath: string; thumbnailPath: string;
}; };
export type PeopleWithFacesResponseDto = {
numberOfFaces: number;
visiblePeople: PersonWithFacesResponseDto[];
};
export type SmartInfoResponseDto = { export type SmartInfoResponseDto = {
objects?: string[] | null; objects?: string[] | null;
tags?: string[] | null; tags?: string[] | null;
@@ -161,7 +134,7 @@ export type AssetResponseDto = {
originalPath: string; originalPath: string;
owner?: UserResponseDto; owner?: UserResponseDto;
ownerId: string; ownerId: string;
people?: PeopleWithFacesResponseDto; people?: PersonWithFacesResponseDto[];
resized: boolean; resized: boolean;
smartInfo?: SmartInfoResponseDto; smartInfo?: SmartInfoResponseDto;
stack?: AssetResponseDto[]; stack?: AssetResponseDto[];
@@ -544,22 +517,26 @@ export type OAuthCallbackDto = {
}; };
export type PartnerResponseDto = { export type PartnerResponseDto = {
avatarColor: UserAvatarColor; avatarColor: UserAvatarColor;
createdAt: string;
deletedAt: string | null;
email: string; email: string;
id: string; id: string;
inTimeline?: boolean; inTimeline?: boolean;
isAdmin: boolean;
memoriesEnabled?: boolean;
name: string; name: string;
oauthId: string;
profileImagePath: string; profileImagePath: string;
quotaSizeInBytes: number | null;
quotaUsageInBytes: number | null;
shouldChangePassword: boolean;
status: UserStatus;
storageLabel: string | null;
updatedAt: string;
}; };
export type UpdatePartnerDto = { export type UpdatePartnerDto = {
inTimeline: boolean; inTimeline: boolean;
}; };
export type AssetFaceUpdateItem = {
assetId: string;
personId: string;
};
export type AssetFaceUpdateDto = {
data: AssetFaceUpdateItem[];
};
export type PeopleResponseDto = { export type PeopleResponseDto = {
hidden: number; hidden: number;
people: PersonResponseDto[]; people: PersonResponseDto[];
@@ -604,6 +581,13 @@ export type PersonUpdateDto = {
export type MergePersonDto = { export type MergePersonDto = {
ids: string[]; ids: string[];
}; };
export type AssetFaceUpdateItem = {
assetId: string;
personId: string;
};
export type AssetFaceUpdateDto = {
data: AssetFaceUpdateItem[];
};
export type PersonStatisticsResponseDto = { export type PersonStatisticsResponseDto = {
assets: number; assets: number;
}; };
@@ -1076,12 +1060,27 @@ export type TimeBucketResponseDto = {
count: number; count: number;
timeBucket: string; timeBucket: string;
}; };
export type UserUpdateMeDto = { export type CreateUserDto = {
email: string;
memoriesEnabled?: boolean;
name: string;
notify?: boolean;
password: string;
quotaSizeInBytes?: number | null;
shouldChangePassword?: boolean;
storageLabel?: string | null;
};
export type UpdateUserDto = {
avatarColor?: UserAvatarColor; avatarColor?: UserAvatarColor;
email?: string; email?: string;
id: string;
isAdmin?: boolean;
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
name?: string; name?: string;
password?: string; password?: string;
quotaSizeInBytes?: number | null;
shouldChangePassword?: boolean;
storageLabel?: string;
}; };
export type CreateProfileImageDto = { export type CreateProfileImageDto = {
file: Blob; file: Blob;
@@ -1090,6 +1089,9 @@ export type CreateProfileImageResponseDto = {
profileImagePath: string; profileImagePath: string;
userId: string; userId: string;
}; };
export type DeleteUserDto = {
force?: boolean;
};
export function getActivities({ albumId, assetId, level, $type, userId }: { export function getActivities({ albumId, assetId, level, $type, userId }: {
albumId: string; albumId: string;
assetId?: string; assetId?: string;
@@ -1144,77 +1146,6 @@ export function deleteActivity({ id }: {
method: "DELETE" method: "DELETE"
})); }));
} }
export function searchUsersAdmin({ withDeleted }: {
withDeleted?: boolean;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: UserAdminResponseDto[];
}>(`/admin/users${QS.query(QS.explode({
withDeleted
}))}`, {
...opts
}));
}
export function createUserAdmin({ userAdminCreateDto }: {
userAdminCreateDto: UserAdminCreateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: UserAdminResponseDto;
}>("/admin/users", oazapfts.json({
...opts,
method: "POST",
body: userAdminCreateDto
})));
}
export function deleteUserAdmin({ id, userAdminDeleteDto }: {
id: string;
userAdminDeleteDto: UserAdminDeleteDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: UserAdminResponseDto;
}>(`/admin/users/${encodeURIComponent(id)}`, oazapfts.json({
...opts,
method: "DELETE",
body: userAdminDeleteDto
})));
}
export function getUserAdmin({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: UserAdminResponseDto;
}>(`/admin/users/${encodeURIComponent(id)}`, {
...opts
}));
}
export function updateUserAdmin({ id, userAdminUpdateDto }: {
id: string;
userAdminUpdateDto: UserAdminUpdateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: UserAdminResponseDto;
}>(`/admin/users/${encodeURIComponent(id)}`, oazapfts.json({
...opts,
method: "PUT",
body: userAdminUpdateDto
})));
}
export function restoreUserAdmin({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: UserAdminResponseDto;
}>(`/admin/users/${encodeURIComponent(id)}/restore`, {
...opts,
method: "POST"
}));
}
export function getAllAlbums({ assetId, shared }: { export function getAllAlbums({ assetId, shared }: {
assetId?: string; assetId?: string;
shared?: boolean; shared?: boolean;
@@ -1658,7 +1589,7 @@ export function signUpAdmin({ signUpDto }: {
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 201; status: 201;
data: UserAdminResponseDto; data: UserResponseDto;
}>("/auth/admin-sign-up", oazapfts.json({ }>("/auth/admin-sign-up", oazapfts.json({
...opts, ...opts,
method: "POST", method: "POST",
@@ -1670,7 +1601,7 @@ export function changePassword({ changePasswordDto }: {
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: UserAdminResponseDto; data: UserResponseDto;
}>("/auth/change-password", oazapfts.json({ }>("/auth/change-password", oazapfts.json({
...opts, ...opts,
method: "POST", method: "POST",
@@ -1771,17 +1702,6 @@ export function getFaces({ id }: {
...opts ...opts
})); }));
} }
export function unassignFace({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AssetFaceResponseDto;
}>(`/faces/${encodeURIComponent(id)}`, {
...opts,
method: "DELETE"
}));
}
export function reassignFacesById({ id, faceDto }: { export function reassignFacesById({ id, faceDto }: {
id: string; id: string;
faceDto: FaceDto; faceDto: FaceDto;
@@ -2014,7 +1934,7 @@ export function linkOAuthAccount({ oAuthCallbackDto }: {
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 201; status: 201;
data: UserAdminResponseDto; data: UserResponseDto;
}>("/oauth/link", oazapfts.json({ }>("/oauth/link", oazapfts.json({
...opts, ...opts,
method: "POST", method: "POST",
@@ -2029,7 +1949,7 @@ export function redirectOAuthToMobile(opts?: Oazapfts.RequestOpts) {
export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) { export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 201; status: 201;
data: UserAdminResponseDto; data: UserResponseDto;
}>("/oauth/unlink", { }>("/oauth/unlink", {
...opts, ...opts,
method: "POST" method: "POST"
@@ -2079,18 +1999,6 @@ export function updatePartner({ id, updatePartnerDto }: {
body: updatePartnerDto body: updatePartnerDto
}))); })));
} }
export function unassignFaces({ assetFaceUpdateDto }: {
assetFaceUpdateDto: AssetFaceUpdateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: BulkIdResponseDto[];
}>("/people", oazapfts.json({
...opts,
method: "DELETE",
body: assetFaceUpdateDto
})));
}
export function getAllPeople({ withHidden }: { export function getAllPeople({ withHidden }: {
withHidden?: boolean; withHidden?: boolean;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
@@ -2779,34 +2687,50 @@ export function restoreAssets({ bulkIdsDto }: {
body: bulkIdsDto body: bulkIdsDto
}))); })));
} }
export function searchUsers(opts?: Oazapfts.RequestOpts) { export function getAllUsers({ isAll }: {
return oazapfts.ok(oazapfts.fetchJson<{ isAll: boolean;
status: 200;
data: UserResponseDto[];
}>("/users", {
...opts
}));
}
export function getMyUser(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: UserAdminResponseDto;
}>("/users/me", {
...opts
}));
}
export function updateMyUser({ userUpdateMeDto }: {
userUpdateMeDto: UserUpdateMeDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: UserAdminResponseDto; data: UserResponseDto[];
}>("/users/me", oazapfts.json({ }>(`/users${QS.query(QS.explode({
isAll
}))}`, {
...opts
}));
}
export function createUser({ createUserDto }: {
createUserDto: CreateUserDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: UserResponseDto;
}>("/users", oazapfts.json({
...opts,
method: "POST",
body: createUserDto
})));
}
export function updateUser({ updateUserDto }: {
updateUserDto: UpdateUserDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: UserResponseDto;
}>("/users", oazapfts.json({
...opts, ...opts,
method: "PUT", method: "PUT",
body: userUpdateMeDto body: updateUserDto
}))); })));
} }
export function getMyUserInfo(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: UserResponseDto;
}>("/users/me", {
...opts
}));
}
export function deleteProfileImage(opts?: Oazapfts.RequestOpts) { export function deleteProfileImage(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/users/profile-image", { return oazapfts.ok(oazapfts.fetchText("/users/profile-image", {
...opts, ...opts,
@@ -2825,7 +2749,20 @@ export function createProfileImage({ createProfileImageDto }: {
body: createProfileImageDto body: createProfileImageDto
}))); })));
} }
export function getUser({ id }: { export function deleteUser({ id, deleteUserDto }: {
id: string;
deleteUserDto: DeleteUserDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: UserResponseDto;
}>(`/users/${encodeURIComponent(id)}`, oazapfts.json({
...opts,
method: "DELETE",
body: deleteUserDto
})));
}
export function getUserById({ id }: {
id: string; id: string;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
@@ -2845,6 +2782,17 @@ export function getProfileImage({ id }: {
...opts ...opts
})); }));
} }
export function restoreUser({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: UserResponseDto;
}>(`/users/${encodeURIComponent(id)}/restore`, {
...opts,
method: "POST"
}));
}
export enum ReactionLevel { export enum ReactionLevel {
Album = "album", Album = "album",
Asset = "asset" Asset = "asset"
@@ -2869,15 +2817,15 @@ export enum UserAvatarColor {
Gray = "gray", Gray = "gray",
Amber = "amber" Amber = "amber"
} }
export enum AlbumUserRole {
Editor = "editor",
Viewer = "viewer"
}
export enum UserStatus { export enum UserStatus {
Active = "active", Active = "active",
Removing = "removing", Removing = "removing",
Deleted = "deleted" Deleted = "deleted"
} }
export enum AlbumUserRole {
Editor = "editor",
Viewer = "viewer"
}
export enum TagTypeEnum { export enum TagTypeEnum {
Object = "OBJECT", Object = "OBJECT",
Face = "FACE", Face = "FACE",
+12 -36
View File
@@ -11,7 +11,7 @@
<p align="center"> <p align="center">
<img src="../design/immich-logo-stacked-light.svg" width="300" title="Login With Custom URL"> <img src="../design/immich-logo-stacked-light.svg" width="300" title="Login With Custom URL">
</p> </p>
<h3 align="center">Immich - Solution de sauvegarde performante et auto-hébergée de photos et de vidéos</h3> <h3 align="center">Immich - Solution de sauvegarde performante et auto-hébergée des photos et des vidéos</h3>
<br/> <br/>
<a href="https://immich.app"> <a href="https://immich.app">
<img src="../design/immich-screenshots.png" title="Main Screenshot"> <img src="../design/immich-screenshots.png" title="Main Screenshot">
@@ -36,16 +36,16 @@
## Clause de non-responsabilité ## Clause de non-responsabilité
- ⚠️ Le projet est en **très fort** développement. - ⚠️ Le projet est en **très fort** développement.
- ⚠️ Attendez-vous à rencontrer des bogues et des changements importants. - ⚠️ Attendez-vous à rencontrer des bugs et des changements importants.
- ⚠️ **N'utilisez pas cette application comme seul support de sauvegarde de vos photos et vos vidéos.** - ⚠️ **N'utilisez pas cette application comme seule façon de sauvegarder vos photos et vos vidéos.**
- ⚠️ Ayez toujours un plan de sauvegarde en [3-2-1](https://www.seagate.com/fr/fr/blog/what-is-a-3-2-1-backup-strategy/) pour vos précieuses photos et vidéos ! - ⚠️ Ayez toujours un plan de sauvegarde en [3-2-1](https://www.seagate.com/fr/fr/blog/what-is-a-3-2-1-backup-strategy/) pour vos précieuses photos et vidéos !
## Sommaire ## Sommaire
- [Documentation officielle](https://immich.app/docs) - [Documentation officielle](https://immich.app/docs)
- [Feuille de route](https://github.com/orgs/immich-app/projects/1) - [Feuille de route](https://github.com/orgs/immich-app/projects/1)
- [Démo](#démo) - [Démo](#demo)
- [Fonctionnalités](#fonctionnalités) - [Fonctionnalités](#features)
- [Introduction](https://immich.app/docs/overview/introduction) - [Introduction](https://immich.app/docs/overview/introduction)
- [Installation](https://immich.app/docs/install/requirements) - [Installation](https://immich.app/docs/install/requirements)
- [Contribution](https://immich.app/docs/overview/support-the-project) - [Contribution](https://immich.app/docs/overview/support-the-project)
@@ -56,31 +56,26 @@ Vous pouvez trouver la documentation principale ainsi que les guides d'installat
## Démo ## Démo
Vous pouvez accéder à la démo en ligne sur https://demo.immich.app Vous pouvez accéder à la démo Web sur https://demo.immich.app
Pour l'application mobile, vous pouvez utiliser `https://demo.immich.app/api` dans le champ `URL du point d'accès au serveur` Pour l'application mobile, vous pouvez utiliser `https://demo.immich.app/api` dans le champ 'URL du point d'accès au serveur'
```bash title="Identifiants pour la démo" ```bash title="Demo Credential"
Les identifiants Les identifiants
email: demo@immich.app email: demo@immich.app
mot de passe: demo mot de passe: demo
``` ```
``` ```
Caractéristiques : Plan gratuit Oracle VM - Amsterdam - 2.4Ghz quatre-cœurs ARM64 CPU, 24GB RAM Caractéristiques: Plan gratuit Oracle VM - Amsterdam - 2.4Ghz quatre-cœurs ARM64 CPU, 24GB RAM
``` ```
## Activités # Fonctionnalités
![Activités](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Image des statistiques Repobeats")
## Fonctionnalités
| Fonctionnalités | Mobile | Web | | Fonctionnalités | Mobile | Web |
| ---------------------------------------------------------------- | ------ | --- | | ---------------------------------------------------------------- | ------ | --- |
| Téléverser et voir les vidéos et photos | Oui | Oui | | Téléverser et voir les vidéos et photos | Oui | Oui |
| Sauvegarde automatique quand l'application est ouverte | Oui | N/A | | Sauvegarde automatique quand l'application est ouverte | Oui | N/A |
| Prévention contre la duplication des photos et des vidéos | Oui | Oui |
| Sélection des albums à sauvegarder | Oui | N/A | | Sélection des albums à sauvegarder | Oui | N/A |
| Télécharger les photos et les vidéos sur l'appareil | Oui | Oui | | Télécharger les photos et les vidéos sur l'appareil | Oui | Oui |
| Support multi-utilisateur | Oui | Oui | | Support multi-utilisateur | Oui | Oui |
@@ -94,32 +89,13 @@ Caractéristiques : Plan gratuit Oracle VM - Amsterdam - 2.4Ghz quatre-cœurs AR
| Défilement virtuel | Oui | Oui | | Défilement virtuel | Oui | Oui |
| Support de l'OAuth | Oui | Oui | | Support de l'OAuth | Oui | Oui |
| Clés d'API | N/A | Oui | | Clés d'API | N/A | Oui |
| Sauvegarde et lecture des LivePhoto/MotionPhoto | Oui | Oui | | Sauvegarde et lecture des LivePhotos | iOS | Oui |
| Support de l'affichage des images à 360° | Non | Oui |
| Structure de stockage définissable | Oui | Oui | | Structure de stockage définissable | Oui | Oui |
| Partage public | Non | Oui | | Partage public | Non | Oui |
| Archives et favoris | Oui | Oui | | Archives et favoris | Oui | Oui |
| Carte globale | Oui | Oui | | Carte globale | Non | Oui |
| Partage entre utilisateurs | Oui | Oui | | Partage entre utilisateurs | Oui | Oui |
| Reconnaissance et regroupement facial | Oui | Oui | | Reconnaissance et regroupement facial | Oui | Oui |
| Souvenirs (il y a x années) | Oui | Oui | | Souvenirs (il y a x années) | Oui | Oui |
| Support hors-ligne | Oui | Non | | Support hors-ligne | Oui | Non |
| Gallerie en lecture seule | Oui | Oui | | Gallerie en lecture seule | Oui | Oui |
| Empilage de photos | Oui | Oui |
## Contributeurs
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>
## Historique des favoris
<a href="https://star-history.com/#immich-app/immich&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
</picture>
</a>
@@ -1,9 +1,9 @@
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander'; import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
import { UserAdminResponseDto } from 'src/dtos/user.dto'; import { UserResponseDto } from 'src/dtos/user.dto';
import { CliService } from 'src/services/cli.service'; import { CliService } from 'src/services/cli.service';
const prompt = (inquirer: InquirerService) => { const prompt = (inquirer: InquirerService) => {
return function ask(admin: UserAdminResponseDto) { return function ask(admin: UserResponseDto) {
const { id, oauthId, email, name } = admin; const { id, oauthId, email, name } = admin;
console.log(`Found Admin: console.log(`Found Admin:
- ID=${id} - ID=${id}
+2 -2
View File
@@ -256,8 +256,8 @@ export const defaults = Object.freeze<SystemConfig>({
modelName: 'ViT-B-32__openai', modelName: 'ViT-B-32__openai',
}, },
duplicateDetection: { duplicateDetection: {
enabled: true, enabled: false,
maxDistance: 0.0155, maxDistance: 0.03,
}, },
facialRecognition: { facialRecognition: {
enabled: true, enabled: true,
+4 -4
View File
@@ -12,7 +12,7 @@ import {
SignUpDto, SignUpDto,
ValidateAccessTokenResponseDto, ValidateAccessTokenResponseDto,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserAdminResponseDto } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { Auth, Authenticated, GetLoginDetails } 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 { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
@@ -40,7 +40,7 @@ export class AuthController {
} }
@Post('admin-sign-up') @Post('admin-sign-up')
signUpAdmin(@Body() dto: SignUpDto): Promise<UserAdminResponseDto> { signUpAdmin(@Body() dto: SignUpDto): Promise<UserResponseDto> {
return this.service.adminSignUp(dto); return this.service.adminSignUp(dto);
} }
@@ -54,8 +54,8 @@ export class AuthController {
@Post('change-password') @Post('change-password')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Authenticated() @Authenticated()
changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserAdminResponseDto> { changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
return this.service.changePassword(auth, dto); return this.service.changePassword(auth, dto).then(mapUser);
} }
@Post('logout') @Post('logout')
+1 -7
View File
@@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Param, Put, Query } from '@nestjs/common'; import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto'; import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto';
@@ -26,10 +26,4 @@ export class FaceController {
): Promise<PersonResponseDto> { ): Promise<PersonResponseDto> {
return this.service.reassignFacesById(auth, id, dto); return this.service.reassignFacesById(auth, id, dto);
} }
@Delete(':id')
@Authenticated()
unassignFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetFaceResponseDto> {
return this.service.unassignFace(auth, id);
}
} }
-2
View File
@@ -27,7 +27,6 @@ import { SystemMetadataController } from 'src/controllers/system-metadata.contro
import { TagController } from 'src/controllers/tag.controller'; import { TagController } from 'src/controllers/tag.controller';
import { TimelineController } from 'src/controllers/timeline.controller'; import { TimelineController } from 'src/controllers/timeline.controller';
import { TrashController } from 'src/controllers/trash.controller'; import { TrashController } from 'src/controllers/trash.controller';
import { UserAdminController } from 'src/controllers/user-admin.controller';
import { UserController } from 'src/controllers/user.controller'; import { UserController } from 'src/controllers/user.controller';
export const controllers = [ export const controllers = [
@@ -60,6 +59,5 @@ export const controllers = [
TagController, TagController,
TimelineController, TimelineController,
TrashController, TrashController,
UserAdminController,
UserController, UserController,
]; ];
+3 -3
View File
@@ -10,7 +10,7 @@ import {
OAuthCallbackDto, OAuthCallbackDto,
OAuthConfigDto, OAuthConfigDto,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserAdminResponseDto } from 'src/dtos/user.dto'; import { UserResponseDto } from 'src/dtos/user.dto';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { Auth, Authenticated, GetLoginDetails } 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'; import { respondWithCookie } from 'src/utils/response';
@@ -53,13 +53,13 @@ export class OAuthController {
@Post('link') @Post('link')
@Authenticated() @Authenticated()
linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserAdminResponseDto> { linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
return this.service.link(auth, dto); return this.service.link(auth, dto);
} }
@Post('unlink') @Post('unlink')
@Authenticated() @Authenticated()
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> { unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.unlink(auth); return this.service.unlink(auth);
} }
} }
+1 -6
View File
@@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Inject, Next, Param, Post, Put, Query, Res } from '@nestjs/common'; import { Body, Controller, Get, Inject, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
@@ -87,11 +87,6 @@ export class PersonController {
return this.service.getAssets(auth, id); return this.service.getAssets(auth, id);
} }
@Delete()
unassignFaces(@Auth() auth: AuthDto, @Body() dto: AssetFaceUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.unassignFaces(auth, dto);
}
@Put(':id/reassign') @Put(':id/reassign')
@Authenticated() @Authenticated()
reassignFaces( reassignFaces(
@@ -1,63 +0,0 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import {
UserAdminCreateDto,
UserAdminDeleteDto,
UserAdminResponseDto,
UserAdminSearchDto,
UserAdminUpdateDto,
} from 'src/dtos/user.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { UserAdminService } from 'src/services/user-admin.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('User')
@Controller('admin/users')
export class UserAdminController {
constructor(private service: UserAdminService) {}
@Get()
@Authenticated({ admin: true })
searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
return this.service.search(auth, dto);
}
@Post()
@Authenticated({ admin: true })
createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
return this.service.create(createUserDto);
}
@Get(':id')
@Authenticated({ admin: true })
getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.get(auth, id);
}
@Put(':id')
@Authenticated({ admin: true })
updateUserAdmin(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UserAdminUpdateDto,
): Promise<UserAdminResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ admin: true })
deleteUserAdmin(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UserAdminDeleteDto,
): Promise<UserAdminResponseDto> {
return this.service.delete(auth, id, dto);
}
@Post(':id/restore')
@Authenticated({ admin: true })
restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.restore(auth, id);
}
}
+42 -18
View File
@@ -10,6 +10,7 @@ import {
Param, Param,
Post, Post,
Put, Put,
Query,
Res, Res,
UploadedFile, UploadedFile,
UseInterceptors, UseInterceptors,
@@ -18,7 +19,7 @@ import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express'; import { NextFunction, Response } from 'express';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor'; import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
@@ -36,28 +37,58 @@ export class UserController {
@Get() @Get()
@Authenticated() @Authenticated()
searchUsers(): Promise<UserResponseDto[]> { getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
return this.service.search(); return this.service.getAll(auth, isAll);
}
@Post()
@Authenticated({ admin: true })
createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
return this.service.create(createUserDto);
} }
@Get('me') @Get('me')
@Authenticated() @Authenticated()
getMyUser(@Auth() auth: AuthDto): UserAdminResponseDto { getMyUserInfo(@Auth() auth: AuthDto): Promise<UserResponseDto> {
return this.service.getMe(auth); return this.service.getMe(auth);
} }
@Put('me')
@Authenticated()
updateMyUser(@Auth() auth: AuthDto, @Body() dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
return this.service.updateMe(auth, dto);
}
@Get(':id') @Get(':id')
@Authenticated() @Authenticated()
getUser(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> { getUserById(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.get(id); return this.service.get(id);
} }
@Delete('profile-image')
@HttpCode(HttpStatus.NO_CONTENT)
@Authenticated()
deleteProfileImage(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteProfileImage(auth);
}
@Delete(':id')
@Authenticated({ admin: true })
deleteUser(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: DeleteUserDto,
): Promise<UserResponseDto> {
return this.service.delete(auth, id, dto);
}
@Post(':id/restore')
@Authenticated({ admin: true })
restoreUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
return this.service.restore(auth, id);
}
// TODO: replace with @Put(':id')
@Put()
@Authenticated()
updateUser(@Auth() auth: AuthDto, @Body() updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
return this.service.update(auth, updateUserDto);
}
@UseInterceptors(FileUploadInterceptor) @UseInterceptors(FileUploadInterceptor)
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto }) @ApiBody({ description: 'A new avatar for the user', type: CreateProfileImageDto })
@@ -70,13 +101,6 @@ export class UserController {
return this.service.createProfileImage(auth, fileInfo); return this.service.createProfileImage(auth, fileInfo);
} }
@Delete('profile-image')
@HttpCode(HttpStatus.NO_CONTENT)
@Authenticated()
deleteProfileImage(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteProfileImage(auth);
}
@Get(':id/profile-image') @Get(':id/profile-image')
@FileResponse() @FileResponse()
@Authenticated() @Authenticated()
+42 -1
View File
@@ -1,6 +1,7 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException, ForbiddenException } from '@nestjs/common';
import sanitize from 'sanitize-filename'; import sanitize from 'sanitize-filename';
import { SALT_ROUNDS } from 'src/constants'; import { SALT_ROUNDS } from 'src/constants';
import { UserResponseDto } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IUserRepository } from 'src/interfaces/user.interface'; import { IUserRepository } from 'src/interfaces/user.interface';
@@ -25,6 +26,46 @@ export class UserCore {
instance = null; instance = null;
} }
// TODO: move auth related checks to the service layer
async updateUser(user: UserEntity | UserResponseDto, id: string, dto: Partial<UserEntity>): Promise<UserEntity> {
if (!user.isAdmin && user.id !== id) {
throw new ForbiddenException('You are not allowed to update this user');
}
if (!user.isAdmin) {
// Users can never update the isAdmin property.
delete dto.isAdmin;
delete dto.storageLabel;
} else if (dto.isAdmin && user.id !== id) {
// Admin cannot create another admin.
throw new BadRequestException('The server already has an admin');
}
if (dto.email) {
const duplicate = await this.userRepository.getByEmail(dto.email);
if (duplicate && duplicate.id !== id) {
throw new BadRequestException('Email already in use by another account');
}
}
if (dto.storageLabel) {
const duplicate = await this.userRepository.getByStorageLabel(dto.storageLabel);
if (duplicate && duplicate.id !== id) {
throw new BadRequestException('Storage label already in use by another account');
}
}
if (dto.password) {
dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
}
if (dto.storageLabel === '') {
dto.storageLabel = null;
}
return this.userRepository.update(id, { ...dto, updatedAt: new Date() });
}
async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> { async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
const user = await this.userRepository.getByEmail(dto.email); const user = await this.userRepository.getByEmail(dto.email);
if (user) { if (user) {
+3 -3
View File
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserDto, mapSimpleUser } from 'src/dtos/user.dto';
import { ActivityEntity } from 'src/entities/activity.entity'; import { ActivityEntity } from 'src/entities/activity.entity';
import { Optional, ValidateUUID } from 'src/validation'; import { Optional, ValidateUUID } from 'src/validation';
@@ -20,7 +20,7 @@ export class ActivityResponseDto {
id!: string; id!: string;
createdAt!: Date; createdAt!: Date;
type!: ReactionType; type!: ReactionType;
user!: UserResponseDto; user!: UserDto;
assetId!: string | null; assetId!: string | null;
comment?: string | null; comment?: string | null;
} }
@@ -73,6 +73,6 @@ export function mapActivity(activity: ActivityEntity): ActivityResponseDto {
createdAt: activity.createdAt, createdAt: activity.createdAt,
comment: activity.comment, comment: activity.comment,
type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT, type: activity.isLiked ? ReactionType.LIKE : ReactionType.COMMENT,
user: mapUser(activity.user), user: mapSimpleUser(activity.user),
}; };
} }
+5 -10
View File
@@ -2,12 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { PropertyLifecycle } from 'src/decorators'; import { PropertyLifecycle } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
import { import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/dtos/person.dto';
PeopleWithFacesResponseDto,
PersonWithFacesResponseDto,
mapFacesWithoutPerson,
mapPerson,
} from 'src/dtos/person.dto';
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
@@ -45,7 +40,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
exifInfo?: ExifResponseDto; exifInfo?: ExifResponseDto;
smartInfo?: SmartInfoResponseDto; smartInfo?: SmartInfoResponseDto;
tags?: TagResponseDto[]; tags?: TagResponseDto[];
people?: PeopleWithFacesResponseDto; people?: PersonWithFacesResponseDto[];
/**base64 encoded sha1 hash */ /**base64 encoded sha1 hash */
checksum!: string; checksum!: string;
stackParentId?: string | null; stackParentId?: string | null;
@@ -61,7 +56,7 @@ export type AssetMapOptions = {
auth?: AuthDto; auth?: AuthDto;
}; };
const peopleWithFaces = (faces: AssetFaceEntity[]): PeopleWithFacesResponseDto => { const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] => {
const result: PersonWithFacesResponseDto[] = []; const result: PersonWithFacesResponseDto[] = [];
if (faces) { if (faces) {
for (const face of faces) { for (const face of faces) {
@@ -76,7 +71,7 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PeopleWithFacesResponseDto =
} }
} }
return { visiblePeople: result, numberOfFaces: faces.length }; return result;
}; };
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto { export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
@@ -120,7 +115,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map(mapTag), tags: entity.tags?.map(mapTag),
people: entity.faces ? peopleWithFaces(entity.faces) : undefined, people: peopleWithFaces(entity.faces),
checksum: entity.checksum.toString('base64'), checksum: entity.checksum.toString('base64'),
stackParentId: withStack ? entity.stack?.primaryAssetId : undefined, stackParentId: withStack ? entity.stack?.primaryAssetId : undefined,
stack: withStack stack: withStack
-6
View File
@@ -77,12 +77,6 @@ export class PersonWithFacesResponseDto extends PersonResponseDto {
faces!: AssetFaceWithoutPersonResponseDto[]; faces!: AssetFaceWithoutPersonResponseDto[];
} }
export class PeopleWithFacesResponseDto {
visiblePeople!: PersonWithFacesResponseDto[];
@ApiProperty({ type: 'integer' })
numberOfFaces!: number;
}
export class AssetFaceWithoutPersonResponseDto { export class AssetFaceWithoutPersonResponseDto {
@ValidateUUID() @ValidateUUID()
id!: string; id!: string;
+22 -7
View File
@@ -1,12 +1,12 @@
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import { UserAdminCreateDto, UserUpdateMeDto } from 'src/dtos/user.dto'; import { CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto';
describe('update user DTO', () => { describe('update user DTO', () => {
it('should allow emails without a tld', async () => { it('should allow emails without a tld', async () => {
const someEmail = 'test@test'; const someEmail = 'test@test';
const dto = plainToInstance(UserUpdateMeDto, { const dto = plainToInstance(UpdateUserDto, {
email: someEmail, email: someEmail,
id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', id: '3fe388e4-2078-44d7-b36c-39d9dee3a657',
}); });
@@ -18,22 +18,22 @@ describe('update user DTO', () => {
describe('create user DTO', () => { describe('create user DTO', () => {
it('validates the email', async () => { it('validates the email', async () => {
const params: Partial<UserAdminCreateDto> = { const params: Partial<CreateUserDto> = {
email: undefined, email: undefined,
password: 'password', password: 'password',
name: 'name', name: 'name',
}; };
let dto: UserAdminCreateDto = plainToInstance(UserAdminCreateDto, params); let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
let errors = await validate(dto); let errors = await validate(dto);
expect(errors).toHaveLength(1); expect(errors).toHaveLength(1);
params.email = 'invalid email'; params.email = 'invalid email';
dto = plainToInstance(UserAdminCreateDto, params); dto = plainToInstance(CreateUserDto, params);
errors = await validate(dto); errors = await validate(dto);
expect(errors).toHaveLength(1); expect(errors).toHaveLength(1);
params.email = 'valid@email.com'; params.email = 'valid@email.com';
dto = plainToInstance(UserAdminCreateDto, params); dto = plainToInstance(CreateUserDto, params);
errors = await validate(dto); errors = await validate(dto);
expect(errors).toHaveLength(0); expect(errors).toHaveLength(0);
}); });
@@ -41,7 +41,7 @@ describe('create user DTO', () => {
it('should allow emails without a tld', async () => { it('should allow emails without a tld', async () => {
const someEmail = 'test@test'; const someEmail = 'test@test';
const dto = plainToInstance(UserAdminCreateDto, { const dto = plainToInstance(CreateUserDto, {
email: someEmail, email: someEmail,
password: 'some password', password: 'some password',
name: 'some name', name: 'some name',
@@ -51,3 +51,18 @@ describe('create user DTO', () => {
expect(dto.email).toEqual(someEmail); expect(dto.email).toEqual(someEmail);
}); });
}); });
describe('create user oauth DTO', () => {
it('should allow emails without a tld', async () => {
const someEmail = 'test@test';
const dto = plainToInstance(CreateUserOAuthDto, {
email: someEmail,
oauthId: 'some oauth id',
name: 'some name',
});
const errors = await validate(dto);
expect(errors).toHaveLength(0);
expect(dto.email).toEqual(someEmail);
});
});
+50 -62
View File
@@ -1,63 +1,12 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
import { UserAvatarColor } from 'src/entities/user-metadata.entity'; import { UserAvatarColor } from 'src/entities/user-metadata.entity';
import { UserEntity, UserStatus } from 'src/entities/user.entity'; import { UserEntity, UserStatus } from 'src/entities/user.entity';
import { getPreferences } from 'src/utils/preferences'; import { getPreferences } from 'src/utils/preferences';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
export class UserUpdateMeDto { export class CreateUserDto {
@Optional()
@IsEmail({ require_tld: false })
@Transform(toEmail)
email?: string;
// TODO: migrate to the other change password endpoint
@Optional()
@IsNotEmpty()
@IsString()
password?: string;
@Optional()
@IsString()
@IsNotEmpty()
name?: string;
@ValidateBoolean({ optional: true })
memoriesEnabled?: boolean;
@Optional()
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor?: UserAvatarColor;
}
export class UserResponseDto {
id!: string;
name!: string;
email!: string;
profileImagePath!: string;
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor!: UserAvatarColor;
}
export const mapUser = (entity: UserEntity): UserResponseDto => {
return {
id: entity.id,
email: entity.email,
name: entity.name,
profileImagePath: entity.profileImagePath,
avatarColor: getPreferences(entity).avatar.color,
};
};
export class UserAdminSearchDto {
@ValidateBoolean({ optional: true })
withDeleted?: boolean;
}
export class UserAdminCreateDto {
@IsEmail({ require_tld: false }) @IsEmail({ require_tld: false })
@Transform(toEmail) @Transform(toEmail)
email!: string; email!: string;
@@ -92,7 +41,23 @@ export class UserAdminCreateDto {
notify?: boolean; notify?: boolean;
} }
export class UserAdminUpdateDto { export class CreateUserOAuthDto {
@IsEmail({ require_tld: false })
@Transform(({ value }) => value?.toLowerCase())
email!: string;
@IsNotEmpty()
oauthId!: string;
name?: string;
}
export class DeleteUserDto {
@ValidateBoolean({ optional: true })
force?: boolean;
}
export class UpdateUserDto {
@Optional() @Optional()
@IsEmail({ require_tld: false }) @IsEmail({ require_tld: false })
@Transform(toEmail) @Transform(toEmail)
@@ -108,10 +73,18 @@ export class UserAdminUpdateDto {
@IsNotEmpty() @IsNotEmpty()
name?: string; name?: string;
@Optional({ nullable: true }) @Optional()
@IsString() @IsString()
@Transform(toSanitized) @Transform(toSanitized)
storageLabel?: string | null; storageLabel?: string;
@IsNotEmpty()
@IsUUID('4')
@ApiProperty({ format: 'uuid' })
id!: string;
@ValidateBoolean({ optional: true })
isAdmin?: boolean;
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
@@ -131,12 +104,17 @@ export class UserAdminUpdateDto {
quotaSizeInBytes?: number | null; quotaSizeInBytes?: number | null;
} }
export class UserAdminDeleteDto { export class UserDto {
@ValidateBoolean({ optional: true }) id!: string;
force?: boolean; name!: string;
email!: string;
profileImagePath!: string;
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor!: UserAvatarColor;
} }
export class UserAdminResponseDto extends UserResponseDto { export class UserResponseDto extends UserDto {
storageLabel!: string | null; storageLabel!: string | null;
shouldChangePassword!: boolean; shouldChangePassword!: boolean;
isAdmin!: boolean; isAdmin!: boolean;
@@ -153,9 +131,19 @@ export class UserAdminResponseDto extends UserResponseDto {
status!: string; status!: string;
} }
export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto { export const mapSimpleUser = (entity: UserEntity): UserDto => {
return { return {
...mapUser(entity), id: entity.id,
email: entity.email,
name: entity.name,
profileImagePath: entity.profileImagePath,
avatarColor: getPreferences(entity).avatar.color,
};
};
export function mapUser(entity: UserEntity): UserResponseDto {
return {
...mapSimpleUser(entity),
storageLabel: entity.storageLabel, storageLabel: entity.storageLabel,
shouldChangePassword: entity.shouldChangePassword, shouldChangePassword: entity.shouldChangePassword,
isAdmin: entity.isAdmin, isAdmin: entity.isAdmin,
-3
View File
@@ -37,9 +37,6 @@ export class AssetFaceEntity {
@Column({ default: 0, type: 'int' }) @Column({ default: 0, type: 'int' })
boundingBoxY2!: number; boundingBoxY2!: number;
@Column({ default: false })
isEdited!: boolean;
@ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) @ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
asset!: AssetEntity; asset!: AssetEntity;
+3 -3
View File
@@ -53,7 +53,7 @@ export interface VideoInfo {
audioStreams: AudioStreamInfo[]; audioStreams: AudioStreamInfo[];
} }
export interface TranscodeCommand { export interface TranscodeOptions {
inputOptions: string[]; inputOptions: string[];
outputOptions: string[]; outputOptions: string[];
twoPass: boolean; twoPass: boolean;
@@ -67,7 +67,7 @@ export interface BitrateDistribution {
} }
export interface VideoCodecSWConfig { export interface VideoCodecSWConfig {
getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream: AudioStreamInfo): TranscodeCommand; getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream: AudioStreamInfo): TranscodeOptions;
} }
export interface VideoCodecHWConfig extends VideoCodecSWConfig { export interface VideoCodecHWConfig extends VideoCodecSWConfig {
@@ -83,5 +83,5 @@ export interface IMediaRepository {
// video // video
probe(input: string): Promise<VideoInfo>; probe(input: string): Promise<VideoInfo>;
transcode(input: string, output: string | Writable, command: TranscodeCommand): Promise<void>; transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise<void>;
} }
+2 -2
View File
@@ -23,7 +23,7 @@ export interface AssetFaceId {
export interface UpdateFacesData { export interface UpdateFacesData {
oldPersonId?: string; oldPersonId?: string;
faceIds?: string[]; faceIds?: string[];
newPersonId: string | null; newPersonId: string;
} }
export interface PersonStatistics { export interface PersonStatistics {
@@ -60,7 +60,7 @@ export interface IPersonRepository {
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>; getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
getRandomFace(personId: string): Promise<AssetFaceEntity | null>; getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
getStatistics(personId: string): Promise<PersonStatistics>; getStatistics(personId: string): Promise<PersonStatistics>;
reassignFace(assetFaceId: string, newPersonId: string | null): Promise<number>; reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
getNumberOfPeople(userId: string): Promise<PeopleStatistics>; getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
reassignFaces(data: UpdateFacesData): Promise<number>; reassignFaces(data: UpdateFacesData): Promise<number>;
update(entity: Partial<PersonEntity>): Promise<PersonEntity>; update(entity: Partial<PersonEntity>): Promise<PersonEntity>;
+1 -2
View File
@@ -155,9 +155,8 @@ export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
export interface AssetDuplicateSearch { export interface AssetDuplicateSearch {
assetId: string; assetId: string;
embedding: Embedding; embedding: Embedding;
maxDistance?: number;
type: AssetType;
userIds: string[]; userIds: string[];
maxDistance?: number;
} }
export interface FaceSearchResult { export interface FaceSearchResult {
@@ -1,13 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddEditedAssetFace1715357609038 implements MigrationInterface {
name = 'AddEditedAssetFace1715357609038';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "asset_faces" ADD "isEdited" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "asset_faces" DROP COLUMN "isEdited"`);
}
}
-1
View File
@@ -153,7 +153,6 @@ FROM
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
WHERE WHERE
"partner"."sharedWithId" = $1 "partner"."sharedWithId" = $1
AND "asset"."isArchived" = false
AND "asset"."id" IN ($2) AND "asset"."id" IN ($2)
-- AccessRepository.asset.checkSharedLinkAccess -- AccessRepository.asset.checkSharedLinkAccess
+1 -5
View File
@@ -22,17 +22,13 @@ FROM
"APIKeyEntity__APIKeyEntity_user"."status" AS "APIKeyEntity__APIKeyEntity_user_status", "APIKeyEntity__APIKeyEntity_user"."status" AS "APIKeyEntity__APIKeyEntity_user_status",
"APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt", "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
"APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes", "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes",
"APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes", "APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes"
"7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_userId",
"7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."key" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_key",
"7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."value" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_value"
FROM FROM
"api_keys" "APIKeyEntity" "api_keys" "APIKeyEntity"
LEFT JOIN "users" "APIKeyEntity__APIKeyEntity_user" ON "APIKeyEntity__APIKeyEntity_user"."id" = "APIKeyEntity"."userId" LEFT JOIN "users" "APIKeyEntity__APIKeyEntity_user" ON "APIKeyEntity__APIKeyEntity_user"."id" = "APIKeyEntity"."userId"
AND ( AND (
"APIKeyEntity__APIKeyEntity_user"."deletedAt" IS NULL "APIKeyEntity__APIKeyEntity_user"."deletedAt" IS NULL
) )
LEFT JOIN "user_metadata" "7f5f7a38bf327bfbbf826778460704c9a50fe6f4" ON "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" = "APIKeyEntity__APIKeyEntity_user"."id"
WHERE WHERE
(("APIKeyEntity"."key" = $1)) (("APIKeyEntity"."key" = $1))
) "distinctAlias" ) "distinctAlias"
-1
View File
@@ -195,7 +195,6 @@ SELECT
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
"AssetEntity__AssetEntity_faces"."isEdited" AS "AssetEntity__AssetEntity_faces_isEdited",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
+3 -10
View File
@@ -71,7 +71,6 @@ SELECT
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
"AssetFaceEntity"."isEdited" AS "AssetFaceEntity_isEdited",
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
@@ -104,7 +103,6 @@ FROM
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
"AssetFaceEntity"."isEdited" AS "AssetFaceEntity_isEdited",
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
@@ -140,7 +138,6 @@ FROM
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
"AssetFaceEntity"."isEdited" AS "AssetFaceEntity_isEdited",
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id", "AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt", "AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt", "AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
@@ -197,10 +194,9 @@ LIMIT
-- PersonRepository.reassignFace -- PersonRepository.reassignFace
UPDATE "asset_faces" UPDATE "asset_faces"
SET SET
"personId" = $1, "personId" = $1
"isEdited" = $2
WHERE WHERE
"id" = $3 "id" = $2
-- PersonRepository.getByName -- PersonRepository.getByName
SELECT SELECT
@@ -287,7 +283,6 @@ FROM
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
"AssetEntity__AssetEntity_faces"."isEdited" AS "AssetEntity__AssetEntity_faces_isEdited",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
@@ -380,7 +375,6 @@ SELECT
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
"AssetFaceEntity"."isEdited" AS "AssetFaceEntity_isEdited",
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id", "AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId", "AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId", "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
@@ -433,8 +427,7 @@ SELECT
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1", "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2"
"AssetFaceEntity"."isEdited" AS "AssetFaceEntity_isEdited"
FROM FROM
"asset_faces" "AssetFaceEntity" "asset_faces" "AssetFaceEntity"
WHERE WHERE
+1 -3
View File
@@ -204,7 +204,6 @@ WITH
"asset"."ownerId" IN ($2) "asset"."ownerId" IN ($2)
AND "asset"."id" != $3 AND "asset"."id" != $3
AND "asset"."isVisible" = $4 AND "asset"."isVisible" = $4
AND "asset"."type" = $5
) )
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
ORDER BY ORDER BY
@@ -217,7 +216,7 @@ SELECT
FROM FROM
"cte" "res" "cte" "res"
WHERE WHERE
res.distance <= $6 res.distance <= $5
-- SearchRepository.searchFaces -- SearchRepository.searchFaces
START TRANSACTION START TRANSACTION
@@ -241,7 +240,6 @@ WITH
"faces"."boundingBoxY1" AS "boundingBoxY1", "faces"."boundingBoxY1" AS "boundingBoxY1",
"faces"."boundingBoxX2" AS "boundingBoxX2", "faces"."boundingBoxX2" AS "boundingBoxX2",
"faces"."boundingBoxY2" AS "boundingBoxY2", "faces"."boundingBoxY2" AS "boundingBoxY2",
"faces"."isEdited" AS "isEdited",
"faces"."embedding" <= > $1 AS "distance" "faces"."embedding" <= > $1 AS "distance"
FROM FROM
"asset_faces" "faces" "asset_faces" "faces"
+1 -5
View File
@@ -38,17 +38,13 @@ FROM
"SessionEntity__SessionEntity_user"."status" AS "SessionEntity__SessionEntity_user_status", "SessionEntity__SessionEntity_user"."status" AS "SessionEntity__SessionEntity_user_status",
"SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt", "SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt",
"SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes", "SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes",
"SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes", "SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes"
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_userId",
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."key" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_key",
"469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."value" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_value"
FROM FROM
"sessions" "SessionEntity" "sessions" "SessionEntity"
LEFT JOIN "users" "SessionEntity__SessionEntity_user" ON "SessionEntity__SessionEntity_user"."id" = "SessionEntity"."userId" LEFT JOIN "users" "SessionEntity__SessionEntity_user" ON "SessionEntity__SessionEntity_user"."id" = "SessionEntity"."userId"
AND ( AND (
"SessionEntity__SessionEntity_user"."deletedAt" IS NULL "SessionEntity__SessionEntity_user"."deletedAt" IS NULL
) )
LEFT JOIN "user_metadata" "469e6aa7ff79eff78f8441f91ba15bb07d3634dd" ON "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" = "SessionEntity__SessionEntity_user"."id"
WHERE WHERE
(("SessionEntity"."token" = $1)) (("SessionEntity"."token" = $1))
) "distinctAlias" ) "distinctAlias"
@@ -240,7 +240,6 @@ class AssetAccess implements IAssetAccess {
.innerJoin('sharedBy.assets', 'asset') .innerJoin('sharedBy.assets', 'asset')
.select('asset.id', 'assetId') .select('asset.id', 'assetId')
.where('partner.sharedWithId = :userId', { userId }) .where('partner.sharedWithId = :userId', { userId })
.andWhere('asset.isArchived = false')
.andWhere('asset.id IN (:...assetIds)', { assetIds: [...assetIds] }) .andWhere('asset.id IN (:...assetIds)', { assetIds: [...assetIds] })
.getRawMany() .getRawMany()
.then((rows) => new Set(rows.map((row) => row.assetId))); .then((rows) => new Set(rows.map((row) => row.assetId)));
@@ -34,9 +34,7 @@ export class ApiKeyRepository implements IKeyRepository {
}, },
where: { key: hashedToken }, where: { key: hashedToken },
relations: { relations: {
user: { user: true,
metadata: true,
},
}, },
}); });
} }
@@ -19,11 +19,9 @@ export class MachineLearningRepository implements IMachineLearningRepository {
private async predict<T>(url: string, input: TextModelInput | VisionModelInput, config: ModelConfig): Promise<T> { private async predict<T>(url: string, input: TextModelInput | VisionModelInput, config: ModelConfig): Promise<T> {
const formData = await this.getFormData(input, config); const formData = await this.getFormData(input, config);
const res = await fetch(new URL('/predict', url), { method: 'POST', body: formData }).catch( const res = await fetch(`${url}/predict`, { method: 'POST', body: formData }).catch((error: Error | any) => {
(error: Error | any) => { throw new Error(`${errorPrefix} to "${url}" failed with ${error?.cause || error}`);
throw new Error(`${errorPrefix} to "${url}" failed with ${error?.cause || error}`); });
},
);
if (res.status >= 400) { if (res.status >= 400) {
const modelType = config.modelType ? ` for ${config.modelType.replace('-', ' ')}` : ''; const modelType = config.modelType ? ` for ${config.modelType.replace('-', ' ')}` : '';
+3 -3
View File
@@ -11,7 +11,7 @@ import {
IMediaRepository, IMediaRepository,
ImageDimensions, ImageDimensions,
ThumbnailOptions, ThumbnailOptions,
TranscodeCommand, TranscodeOptions,
VideoInfo, VideoInfo,
} from 'src/interfaces/media.interface'; } from 'src/interfaces/media.interface';
import { Instrumentation } from 'src/utils/instrumentation'; import { Instrumentation } from 'src/utils/instrumentation';
@@ -97,7 +97,7 @@ export class MediaRepository implements IMediaRepository {
}; };
} }
transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise<void> { transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise<void> {
if (!options.twoPass) { if (!options.twoPass) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run(); this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run();
@@ -150,7 +150,7 @@ export class MediaRepository implements IMediaRepository {
return { width, height }; return { width, height };
} }
private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) { 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)
.outputOptions(options.outputOptions) .outputOptions(options.outputOptions)
+1 -1
View File
@@ -148,7 +148,7 @@ export class PersonRepository implements IPersonRepository {
const result = await this.assetFaceRepository const result = await this.assetFaceRepository
.createQueryBuilder() .createQueryBuilder()
.update() .update()
.set({ personId: newPersonId, isEdited: true }) .set({ personId: newPersonId })
.where({ id: assetFaceId }) .where({ id: assetFaceId })
.execute(); .execute();
+4 -9
View File
@@ -160,7 +160,6 @@ export class SearchRepository implements ISearchRepository {
assetId, assetId,
embedding, embedding,
maxDistance, maxDistance,
type,
userIds, userIds,
}: AssetDuplicateSearch): Promise<AssetDuplicateResult[]> { }: AssetDuplicateSearch): Promise<AssetDuplicateResult[]> {
const cte = this.assetRepository.createQueryBuilder('asset'); const cte = this.assetRepository.createQueryBuilder('asset');
@@ -172,22 +171,18 @@ export class SearchRepository implements ISearchRepository {
.where('asset.ownerId IN (:...userIds )') .where('asset.ownerId IN (:...userIds )')
.andWhere('asset.id != :assetId') .andWhere('asset.id != :assetId')
.andWhere('asset.isVisible = :isVisible') .andWhere('asset.isVisible = :isVisible')
.andWhere('asset.type = :type')
.orderBy('search.embedding <=> :embedding') .orderBy('search.embedding <=> :embedding')
.limit(64) .limit(64)
.setParameters({ assetId, embedding: asVector(embedding), isVisible: true, type, userIds }); .setParameters({ assetId, embedding: asVector(embedding), isVisible: true, userIds });
const builder = this.assetRepository.manager const builder = this.assetRepository.manager
.createQueryBuilder() .createQueryBuilder()
.addCommonTableExpression(cte, 'cte') .addCommonTableExpression(cte, 'cte')
.from('cte', 'res') .from('cte', 'res')
.select('res.*'); .select('res.*')
.where('res.distance <= :maxDistance', { maxDistance });
if (maxDistance) { return builder.getRawMany() as any as Promise<AssetDuplicateResult[]>;
builder.where('res.distance <= :maxDistance', { maxDistance });
}
return builder.getRawMany() as Promise<AssetDuplicateResult[]>;
} }
@GenerateSql({ @GenerateSql({
@@ -18,14 +18,7 @@ export class SessionRepository implements ISessionRepository {
@GenerateSql({ params: [DummyValue.STRING] }) @GenerateSql({ params: [DummyValue.STRING] })
getByToken(token: string): Promise<SessionEntity | null> { getByToken(token: string): Promise<SessionEntity | null> {
return this.repository.findOne({ return this.repository.findOne({ where: { token }, relations: { user: true } });
where: { token },
relations: {
user: {
metadata: true,
},
},
});
} }
getByUserId(userId: string): Promise<SessionEntity[]> { getByUserId(userId: string): Promise<SessionEntity[]> {
+1 -1
View File
@@ -266,7 +266,7 @@ export class AssetService {
} }
if (data.ownerId !== auth.user.id || auth.sharedLink) { if (data.ownerId !== auth.user.id || auth.sharedLink) {
delete data.people; data.people = [];
} }
return data; return data;
-1
View File
@@ -138,7 +138,6 @@ describe('AuthService', () => {
email: 'test@immich.com', email: 'test@immich.com',
password: 'hash-password', password: 'hash-password',
} as UserEntity); } as UserEntity);
userMock.update.mockResolvedValue(userStub.user1);
await sut.changePassword(auth, dto); await sut.changePassword(auth, dto);
+10 -17
View File
@@ -11,7 +11,7 @@ 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 { SystemConfig } from 'src/config'; import { SystemConfig } from 'src/config';
import { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; import { AuthType, LOGIN_URL, MOBILE_REDIRECT } from 'src/constants';
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 {
@@ -27,7 +27,7 @@ import {
SignUpDto, SignUpDto,
mapLoginResponse, mapLoginResponse,
} from 'src/dtos/auth.dto'; } from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@@ -109,7 +109,7 @@ export class AuthService {
}; };
} }
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> { async changePassword(auth: AuthDto, dto: ChangePasswordDto) {
const { password, newPassword } = dto; const { password, newPassword } = dto;
const user = await this.userRepository.getByEmail(auth.user.email, true); const user = await this.userRepository.getByEmail(auth.user.email, true);
if (!user) { if (!user) {
@@ -121,14 +121,10 @@ export class AuthService {
throw new BadRequestException('Wrong password'); throw new BadRequestException('Wrong password');
} }
const hashedPassword = await this.cryptoRepository.hashBcrypt(newPassword, SALT_ROUNDS); return this.userCore.updateUser(auth.user, auth.user.id, { password: newPassword });
const updatedUser = await this.userRepository.update(user.id, { password: hashedPassword });
return mapUserAdmin(updatedUser);
} }
async adminSignUp(dto: SignUpDto): Promise<UserAdminResponseDto> { async adminSignUp(dto: SignUpDto): Promise<UserResponseDto> {
const adminUser = await this.userRepository.getAdmin(); const adminUser = await this.userRepository.getAdmin();
if (adminUser) { if (adminUser) {
throw new BadRequestException('The server already has an admin'); throw new BadRequestException('The server already has an admin');
@@ -142,7 +138,7 @@ export class AuthService {
storageLabel: 'admin', storageLabel: 'admin',
}); });
return mapUserAdmin(admin); return mapUser(admin);
} }
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> { async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
@@ -241,7 +237,7 @@ export class AuthService {
return this.createLoginResponse(user, loginDetails); return this.createLoginResponse(user, loginDetails);
} }
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> { async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
const config = await this.configCore.getConfig(); const config = await this.configCore.getConfig();
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url); const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
const duplicate = await this.userRepository.getByOAuthId(oauthId); const duplicate = await this.userRepository.getByOAuthId(oauthId);
@@ -249,14 +245,11 @@ export class AuthService {
this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`); this.logger.warn(`OAuth link account failed: sub is already linked to another user (${duplicate.email}).`);
throw new BadRequestException('This OAuth account has already been linked to another user.'); throw new BadRequestException('This OAuth account has already been linked to another user.');
} }
return mapUser(await this.userRepository.update(auth.user.id, { oauthId }));
const user = await this.userRepository.update(auth.user.id, { oauthId });
return mapUserAdmin(user);
} }
async unlink(auth: AuthDto): Promise<UserAdminResponseDto> { async unlink(auth: AuthDto): Promise<UserResponseDto> {
const user = await this.userRepository.update(auth.user.id, { oauthId: '' }); return mapUser(await this.userRepository.update(auth.user.id, { oauthId: '' }));
return mapUserAdmin(user);
} }
private async getLogoutEndpoint(authType: AuthType): Promise<string> { private async getLogoutEndpoint(authType: AuthType): Promise<string> {
+9 -8
View File
@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { SALT_ROUNDS } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserCore } from 'src/cores/user.core';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
@@ -10,6 +10,7 @@ import { IUserRepository } from 'src/interfaces/user.interface';
@Injectable() @Injectable()
export class CliService { export class CliService {
private configCore: SystemConfigCore; private configCore: SystemConfigCore;
private userCore: UserCore;
constructor( constructor(
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@@ -17,26 +18,26 @@ export class CliService {
@Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IUserRepository) private userRepository: IUserRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository,
) { ) {
this.userCore = UserCore.create(cryptoRepository, userRepository);
this.logger.setContext(CliService.name); this.logger.setContext(CliService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
} }
async listUsers(): Promise<UserAdminResponseDto[]> { async listUsers(): Promise<UserResponseDto[]> {
const users = await this.userRepository.getList({ withDeleted: true }); const users = await this.userRepository.getList({ withDeleted: true });
return users.map((user) => mapUserAdmin(user)); return users.map((user) => mapUser(user));
} }
async resetAdminPassword(ask: (admin: UserAdminResponseDto) => Promise<string | undefined>) { async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) {
const admin = await this.userRepository.getAdmin(); const admin = await this.userRepository.getAdmin();
if (!admin) { if (!admin) {
throw new Error('Admin account does not exist'); throw new Error('Admin account does not exist');
} }
const providedPassword = await ask(mapUserAdmin(admin)); const providedPassword = await ask(mapUser(admin));
const password = providedPassword || this.cryptoRepository.newPassword(24); const password = providedPassword || this.cryptoRepository.newPassword(24);
const hashedPassword = await this.cryptoRepository.hashBcrypt(password, SALT_ROUNDS);
await this.userRepository.update(admin.id, { password: hashedPassword }); await this.userCore.updateUser(admin, admin.id, { password });
return { admin, password, provided: !!providedPassword }; return { admin, password, provided: !!providedPassword };
} }
@@ -214,8 +214,7 @@ describe(SearchService.name, () => {
expect(searchMock.searchDuplicates).toHaveBeenCalledWith({ expect(searchMock.searchDuplicates).toHaveBeenCalledWith({
assetId: assetStub.hasEmbedding.id, assetId: assetStub.hasEmbedding.id,
embedding: assetStub.hasEmbedding.smartSearch!.embedding, embedding: assetStub.hasEmbedding.smartSearch!.embedding,
maxDistance: 0.0155, maxDistance: 0.03,
type: assetStub.hasEmbedding.type,
userIds: [assetStub.hasEmbedding.ownerId], userIds: [assetStub.hasEmbedding.ownerId],
}); });
expect(assetMock.updateDuplicates).toHaveBeenCalledWith({ expect(assetMock.updateDuplicates).toHaveBeenCalledWith({
@@ -240,8 +239,7 @@ describe(SearchService.name, () => {
expect(searchMock.searchDuplicates).toHaveBeenCalledWith({ expect(searchMock.searchDuplicates).toHaveBeenCalledWith({
assetId: assetStub.hasEmbedding.id, assetId: assetStub.hasEmbedding.id,
embedding: assetStub.hasEmbedding.smartSearch!.embedding, embedding: assetStub.hasEmbedding.smartSearch!.embedding,
maxDistance: 0.0155, maxDistance: 0.03,
type: assetStub.hasEmbedding.type,
userIds: [assetStub.hasEmbedding.ownerId], userIds: [assetStub.hasEmbedding.ownerId],
}); });
expect(assetMock.updateDuplicates).toHaveBeenCalledWith({ expect(assetMock.updateDuplicates).toHaveBeenCalledWith({
-1
View File
@@ -94,7 +94,6 @@ export class DuplicateService {
assetId: asset.id, assetId: asset.id,
embedding: asset.smartSearch.embedding, embedding: asset.smartSearch.embedding,
maxDistance: machineLearning.duplicateDetection.maxDistance, maxDistance: machineLearning.duplicateDetection.maxDistance,
type: asset.type,
userIds: [asset.ownerId], userIds: [asset.ownerId],
}); });
-2
View File
@@ -33,7 +33,6 @@ import { SystemMetadataService } from 'src/services/system-metadata.service';
import { TagService } from 'src/services/tag.service'; 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 { UserAdminService } from 'src/services/user-admin.service';
import { UserService } from 'src/services/user.service'; import { UserService } from 'src/services/user.service';
import { VersionService } from 'src/services/version.service'; import { VersionService } from 'src/services/version.service';
@@ -74,6 +73,5 @@ export const services = [
TimelineService, TimelineService,
TrashService, TrashService,
UserService, UserService,
UserAdminService,
VersionService, VersionService,
]; ];
+6 -12
View File
@@ -294,13 +294,11 @@ describe(MediaService.name, () => {
'/original/path.ext', '/original/path.ext',
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
{ {
inputOptions: ['-skip_frame nokey', '-sws_flags accurate_rnd+full_chroma_int'], inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
outputOptions: [ outputOptions: [
'-fps_mode vfr',
'-frames:v 1', '-frames:v 1',
'-update 1',
'-v verbose', '-v verbose',
`-vf fps=12,thumbnail=12,select=gt(scene\\,0.1)+gt(n\\,20),scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_color_matrix=601:out_range=pc,format=yuv420p`, '-vf scale=-2:1440:flags=lanczos+accurate_rnd+bitexact+full_chroma_int:out_color_matrix=601:out_range=pc,format=yuv420p',
], ],
twoPass: false, twoPass: false,
}, },
@@ -321,13 +319,11 @@ describe(MediaService.name, () => {
'/original/path.ext', '/original/path.ext',
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
{ {
inputOptions: ['-skip_frame nokey', '-sws_flags accurate_rnd+full_chroma_int'], inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
outputOptions: [ outputOptions: [
'-fps_mode vfr',
'-frames:v 1', '-frames:v 1',
'-update 1',
'-v verbose', '-v verbose',
`-vf fps=12,thumbnail=12,select=gt(scene\\,0.1)+gt(n\\,20),zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p`, '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p',
], ],
twoPass: false, twoPass: false,
}, },
@@ -350,13 +346,11 @@ describe(MediaService.name, () => {
'/original/path.ext', '/original/path.ext',
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
{ {
inputOptions: ['-skip_frame nokey', '-sws_flags accurate_rnd+full_chroma_int'], inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
outputOptions: [ outputOptions: [
'-fps_mode vfr',
'-frames:v 1', '-frames:v 1',
'-update 1',
'-v verbose', '-v verbose',
`-vf fps=12,thumbnail=12,select=gt(scene\\,0.1)+gt(n\\,20),zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p`, '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p',
], ],
twoPass: false, twoPass: false,
}, },
+111 -37
View File
@@ -27,12 +27,25 @@ import {
QueueName, QueueName,
} 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 { AudioStreamInfo, IMediaRepository, VideoStreamInfo } from 'src/interfaces/media.interface'; import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from 'src/interfaces/media.interface';
import { IMoveRepository } from 'src/interfaces/move.interface'; 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 { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import {
AV1Config,
H264Config,
HEVCConfig,
NvencHwDecodeConfig,
NvencSwDecodeConfig,
QsvHwDecodeConfig,
QsvSwDecodeConfig,
RkmppHwDecodeConfig,
RkmppSwDecodeConfig,
ThumbnailConfig,
VAAPIConfig,
VP9Config,
} from 'src/utils/media';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { usePagination } from 'src/utils/pagination'; import { usePagination } from 'src/utils/pagination';
@@ -40,8 +53,8 @@ import { usePagination } from 'src/utils/pagination';
export class MediaService { export class MediaService {
private configCore: SystemConfigCore; private configCore: SystemConfigCore;
private storageCore: StorageCore; private storageCore: StorageCore;
private maliOpenCL?: boolean; private openCL: boolean | null = null;
private devices?: string[]; private devices: string[] | null = null;
constructor( constructor(
@Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository,
@@ -219,8 +232,8 @@ export class MediaService {
return; return;
} }
const mainAudioStream = this.getMainStream(audioStreams); const mainAudioStream = this.getMainStream(audioStreams);
const config = ThumbnailConfig.create({ ...ffmpeg, targetResolution: size.toString() }); const config = { ...ffmpeg, targetResolution: size.toString() };
const options = config.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream); const options = new ThumbnailConfig(config).getOptions(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream);
await this.mediaRepository.transcode(asset.originalPath, path, options); await this.mediaRepository.transcode(asset.originalPath, path, options);
break; break;
} }
@@ -318,8 +331,8 @@ export class MediaService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const { ffmpeg } = await this.configCore.getConfig(); const { ffmpeg: config } = await this.configCore.getConfig();
const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream); const target = this.getTranscodeTarget(config, mainVideoStream, mainAudioStream);
if (target === TranscodeTarget.NONE) { if (target === TranscodeTarget.NONE) {
if (asset.encodedVideoPath) { if (asset.encodedVideoPath) {
this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`); this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`);
@@ -330,28 +343,30 @@ export class MediaService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
let command; let transcodeOptions;
try { try {
const config = BaseConfig.create(ffmpeg, await this.getDevices(), await this.hasMaliOpenCL()); transcodeOptions = await this.getCodecConfig(config).then((c) =>
command = config.getCommand(target, mainVideoStream, mainAudioStream); c.getOptions(target, mainVideoStream, mainAudioStream),
);
} catch (error) { } catch (error) {
this.logger.error(`An error occurred while configuring transcoding options: ${error}`); this.logger.error(`An error occurred while configuring transcoding options: ${error}`);
return JobStatus.FAILED; return JobStatus.FAILED;
} }
this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(command)}`); this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`);
try { try {
await this.mediaRepository.transcode(input, output, command); await this.mediaRepository.transcode(input, output, transcodeOptions);
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
if (ffmpeg.accel !== TranscodeHWAccel.DISABLED) { if (config.accel !== TranscodeHWAccel.DISABLED) {
this.logger.error( this.logger.error(
`Error occurred during transcoding. Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled.`, `Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`,
); );
} }
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED }); transcodeOptions = await this.getCodecConfig({ ...config, accel: TranscodeHWAccel.DISABLED }).then((c) =>
command = config.getCommand(target, mainVideoStream, mainAudioStream); c.getOptions(target, mainVideoStream, mainAudioStream),
await this.mediaRepository.transcode(input, output, command); );
await this.mediaRepository.transcode(input, output, transcodeOptions);
} }
this.logger.log(`Successfully encoded ${asset.id}`); this.logger.log(`Successfully encoded ${asset.id}`);
@@ -367,10 +382,10 @@ export class MediaService {
private getTranscodeTarget( private getTranscodeTarget(
config: SystemConfigFFmpegDto, config: SystemConfigFFmpegDto,
videoStream?: VideoStreamInfo, videoStream: VideoStreamInfo | null,
audioStream?: AudioStreamInfo, audioStream: AudioStreamInfo | null,
): TranscodeTarget { ): TranscodeTarget {
if (!videoStream && !audioStream) { if (videoStream == null && audioStream == null) {
return TranscodeTarget.NONE; return TranscodeTarget.NONE;
} }
@@ -392,8 +407,8 @@ export class MediaService {
return TranscodeTarget.NONE; return TranscodeTarget.NONE;
} }
private isAudioTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream?: AudioStreamInfo): boolean { private isAudioTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: AudioStreamInfo | null): boolean {
if (!stream) { if (stream == null) {
return false; return false;
} }
@@ -415,8 +430,8 @@ export class MediaService {
} }
} }
private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream?: VideoStreamInfo): boolean { private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo | null): boolean {
if (!stream) { if (stream == null) {
return false; return false;
} }
@@ -450,6 +465,70 @@ export class MediaService {
} }
} }
async getCodecConfig(config: SystemConfigFFmpegDto) {
if (config.accel === TranscodeHWAccel.DISABLED) {
return this.getSWCodecConfig(config);
}
return this.getHWCodecConfig(config);
}
private getSWCodecConfig(config: SystemConfigFFmpegDto) {
switch (config.targetVideoCodec) {
case VideoCodec.H264: {
return new H264Config(config);
}
case VideoCodec.HEVC: {
return new HEVCConfig(config);
}
case VideoCodec.VP9: {
return new VP9Config(config);
}
case VideoCodec.AV1: {
return new AV1Config(config);
}
default: {
throw new UnsupportedMediaTypeException(`Codec '${config.targetVideoCodec}' is unsupported`);
}
}
}
private async getHWCodecConfig(config: SystemConfigFFmpegDto) {
let handler: VideoCodecHWConfig;
switch (config.accel) {
case TranscodeHWAccel.NVENC: {
handler = config.accelDecode ? new NvencHwDecodeConfig(config) : new NvencSwDecodeConfig(config);
break;
}
case TranscodeHWAccel.QSV: {
handler = config.accelDecode
? new QsvHwDecodeConfig(config, await this.getDevices())
: new QsvSwDecodeConfig(config, await this.getDevices());
break;
}
case TranscodeHWAccel.VAAPI: {
handler = new VAAPIConfig(config, await this.getDevices());
break;
}
case TranscodeHWAccel.RKMPP: {
handler =
config.accelDecode && (await this.hasOpenCL())
? new RkmppHwDecodeConfig(config, await this.getDevices())
: new RkmppSwDecodeConfig(config, await this.getDevices());
break;
}
default: {
throw new UnsupportedMediaTypeException(`${config.accel.toUpperCase()} acceleration is unsupported`);
}
}
if (!handler.getSupportedCodecs().includes(config.targetVideoCodec)) {
throw new UnsupportedMediaTypeException(
`${config.accel.toUpperCase()} acceleration does not support codec '${config.targetVideoCodec.toUpperCase()}'. Supported codecs: ${handler.getSupportedCodecs()}`,
);
}
return handler;
}
isSRGB(asset: AssetEntity): boolean { isSRGB(asset: AssetEntity): boolean {
const { colorspace, profileDescription, bitsPerSample } = asset.exifInfo ?? {}; const { colorspace, profileDescription, bitsPerSample } = asset.exifInfo ?? {};
if (colorspace || profileDescription) { if (colorspace || profileDescription) {
@@ -488,29 +567,24 @@ export class MediaService {
private async getDevices() { private async getDevices() {
if (!this.devices) { if (!this.devices) {
try { this.devices = await this.storageRepository.readdir('/dev/dri');
this.devices = await this.storageRepository.readdir('/dev/dri');
} catch {
this.logger.debug('No devices found in /dev/dri.');
this.devices = [];
}
} }
return this.devices; return this.devices;
} }
private async hasMaliOpenCL() { private async hasOpenCL() {
if (this.maliOpenCL === undefined) { if (this.openCL === null) {
try { try {
const maliIcdStat = await this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd'); const maliIcdStat = await this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd');
const maliDeviceStat = await this.storageRepository.stat('/dev/mali0'); const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice(); this.openCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
} catch { } catch {
this.logger.debug('OpenCL not available for transcoding, using CPU decoding instead.'); this.logger.warn('OpenCL not available for transcoding, using CPU instead.');
this.maliOpenCL = false; this.openCL = false;
} }
} }
return this.maliOpenCL; return this.openCL;
} }
} }
+44 -3
View File
@@ -1,4 +1,6 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { PartnerResponseDto } from 'src/dtos/partner.dto';
import { UserAvatarColor } from 'src/entities/user-metadata.entity';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface'; import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface';
import { PartnerService } from 'src/services/partner.service'; import { PartnerService } from 'src/services/partner.service';
@@ -7,6 +9,45 @@ import { partnerStub } from 'test/fixtures/partner.stub';
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
import { Mocked } from 'vitest'; import { Mocked } from 'vitest';
const responseDto = {
admin: <PartnerResponseDto>{
email: 'admin@test.com',
name: 'admin_name',
id: 'admin_id',
isAdmin: true,
oauthId: '',
profileImagePath: '',
shouldChangePassword: false,
storageLabel: 'admin',
createdAt: new Date('2021-01-01'),
deletedAt: null,
updatedAt: new Date('2021-01-01'),
memoriesEnabled: true,
avatarColor: UserAvatarColor.GRAY,
quotaSizeInBytes: null,
inTimeline: true,
quotaUsageInBytes: 0,
},
user1: <PartnerResponseDto>{
email: 'immich@test.com',
name: 'immich_name',
id: 'user-id',
isAdmin: false,
oauthId: '',
profileImagePath: '',
shouldChangePassword: false,
storageLabel: null,
createdAt: new Date('2021-01-01'),
deletedAt: null,
updatedAt: new Date('2021-01-01'),
memoriesEnabled: true,
avatarColor: UserAvatarColor.PRIMARY,
inTimeline: true,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
},
};
describe(PartnerService.name, () => { describe(PartnerService.name, () => {
let sut: PartnerService; let sut: PartnerService;
let partnerMock: Mocked<IPartnerRepository>; let partnerMock: Mocked<IPartnerRepository>;
@@ -24,13 +65,13 @@ describe(PartnerService.name, () => {
describe('getAll', () => { describe('getAll', () => {
it("should return a list of partners with whom I've shared my library", async () => { it("should return a list of partners with whom I've shared my library", async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toBeDefined(); await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toEqual([responseDto.admin]);
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
}); });
it('should return a list of partners who have shared their libraries with me', async () => { it('should return a list of partners who have shared their libraries with me', async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]); partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toBeDefined(); await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toEqual([responseDto.admin]);
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id); expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
}); });
}); });
@@ -40,7 +81,7 @@ describe(PartnerService.name, () => {
partnerMock.get.mockResolvedValue(null); partnerMock.get.mockResolvedValue(null);
partnerMock.create.mockResolvedValue(partnerStub.adminToUser1); partnerMock.create.mockResolvedValue(partnerStub.adminToUser1);
await expect(sut.create(authStub.admin, authStub.user1.user.id)).resolves.toBeDefined(); await expect(sut.create(authStub.admin, authStub.user1.user.id)).resolves.toEqual(responseDto.user1);
expect(partnerMock.create).toHaveBeenCalledWith({ expect(partnerMock.create).toHaveBeenCalledWith({
sharedById: authStub.admin.user.id, sharedById: authStub.admin.user.id,
+4 -4
View File
@@ -25,7 +25,7 @@ export class PartnerService {
} }
const partner = await this.repository.create(partnerId); const partner = await this.repository.create(partnerId);
return this.mapPartner(partner, PartnerDirection.SharedBy); return this.mapToPartnerEntity(partner, PartnerDirection.SharedBy);
} }
async remove(auth: AuthDto, sharedWithId: string): Promise<void> { async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
@@ -44,7 +44,7 @@ export class PartnerService {
return partners return partners
.filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users .filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users
.filter((partner) => partner[key] === auth.user.id) .filter((partner) => partner[key] === auth.user.id)
.map((partner) => this.mapPartner(partner, direction)); .map((partner) => this.mapToPartnerEntity(partner, direction));
} }
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> { async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
@@ -52,10 +52,10 @@ export class PartnerService {
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
return this.mapPartner(entity, PartnerDirection.SharedWith); return this.mapToPartnerEntity(entity, PartnerDirection.SharedWith);
} }
private mapPartner(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto { private mapToPartnerEntity(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto {
// this is opposite to return the non-me user of the "partner" // this is opposite to return the non-me user of the "partner"
const user = mapUser( const user = mapUser(
direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy, direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy,
+1 -58
View File
@@ -438,60 +438,6 @@ describe(PersonService.name, () => {
}); });
}); });
describe('unassignFace', () => {
it('should unassign a face', async () => {
personMock.getFaceById.mockResolvedValueOnce(faceStub.face1);
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
accessMock.person.checkFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id]));
personMock.reassignFace.mockResolvedValue(1);
personMock.getRandomFace.mockResolvedValue(null);
personMock.getFaceById.mockResolvedValueOnce(faceStub.unassignedFace);
await expect(sut.unassignFace(authStub.admin, faceStub.face1.id)).resolves.toStrictEqual(
mapFaces(faceStub.unassignedFace, authStub.admin),
);
expect(mediaMock.generateThumbnail).not.toHaveBeenCalled();
});
it('should not unassign a face if user has no create access', async () => {
personMock.getFaceById.mockResolvedValueOnce(faceStub.face1);
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
personMock.reassignFace.mockResolvedValue(1);
personMock.getRandomFace.mockResolvedValue(null);
personMock.getFaceById.mockResolvedValueOnce(faceStub.unassignedFace);
await expect(sut.unassignFace(authStub.admin, faceStub.face1.id)).rejects.toBeInstanceOf(BadRequestException);
});
});
describe('unassignFaces', () => {
it('should unassign a face', async () => {
personMock.getFacesByIds.mockResolvedValueOnce([faceStub.face1]);
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
accessMock.person.checkFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id]));
personMock.reassignFace.mockResolvedValue(1);
personMock.getRandomFace.mockResolvedValue(null);
personMock.getFaceById.mockResolvedValueOnce(faceStub.unassignedFace);
await expect(
sut.unassignFaces(authStub.admin, { data: [{ assetId: faceStub.face1.id, personId: 'person-1' }] }),
).resolves.toStrictEqual([{ id: 'assetFaceId1', success: true }]);
});
it('should not unassign a face if the user has no create access', async () => {
personMock.getFacesByIds.mockResolvedValueOnce([faceStub.face1]);
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
personMock.reassignFace.mockResolvedValue(1);
personMock.getRandomFace.mockResolvedValue(null);
personMock.getFaceById.mockResolvedValueOnce(faceStub.unassignedFace);
await expect(
sut.unassignFaces(authStub.admin, { data: [{ assetId: faceStub.face1.id, personId: 'person-1' }] }),
).rejects.toBeInstanceOf(BadRequestException);
});
});
describe('handlePersonCleanup', () => { describe('handlePersonCleanup', () => {
it('should delete people without faces', async () => { it('should delete people without faces', async () => {
personMock.getAllWithoutFaces.mockResolvedValue([personStub.noName]); personMock.getAllWithoutFaces.mockResolvedValue([personStub.noName]);
@@ -605,10 +551,7 @@ describe(PersonService.name, () => {
await sut.handleQueueRecognizeFaces({}); await sut.handleQueueRecognizeFaces({});
expect(personMock.getAllFaces).toHaveBeenCalledWith( expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { personId: IsNull() } });
{ skip: 0, take: 1000 },
{ where: { personId: IsNull(), isEdited: false } },
);
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.FACIAL_RECOGNITION, name: JobName.FACIAL_RECOGNITION,
+1 -45
View File
@@ -102,22 +102,6 @@ export class PersonService {
}; };
} }
async unassignFace(auth: AuthDto, id: string): Promise<AssetFaceResponseDto> {
let face = await this.repository.getFaceById(id);
await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id);
if (face.personId) {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, face.personId);
}
await this.repository.reassignFace(face.id, null);
if (face.person && face.person.faceAssetId === face.id) {
await this.createNewFeaturePhoto([face.person.id]);
}
face = await this.repository.getFaceById(id);
return mapFaces(face, auth);
}
async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> { async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
const person = await this.findOrFail(personId); const person = await this.findOrFail(personId);
@@ -147,34 +131,6 @@ export class PersonService {
return result; return result;
} }
async unassignFaces(auth: AuthDto, dto: AssetFaceUpdateDto): Promise<BulkIdResponseDto[]> {
const changeFeaturePhoto: string[] = [];
const results: BulkIdResponseDto[] = [];
for (const data of dto.data) {
const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
for (const face of faces) {
await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id);
if (face.personId) {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, face.personId);
}
await this.repository.reassignFace(face.id, null);
if (face.person && face.person.faceAssetId === face.id) {
changeFeaturePhoto.push(face.person.id);
}
results.push({ id: face.id, success: true });
}
}
if (changeFeaturePhoto.length > 0) {
// Remove duplicates
await this.createNewFeaturePhoto([...changeFeaturePhoto]);
}
return results;
}
async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> { async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId);
@@ -431,7 +387,7 @@ export class PersonService {
} }
const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.repository.getAllFaces(pagination, { where: force ? undefined : { personId: IsNull(), isEdited: false } }), this.repository.getAllFaces(pagination, { where: force ? undefined : { personId: IsNull() } }),
); );
for await (const page of facePagination) { for await (const page of facePagination) {
@@ -149,7 +149,7 @@ describe(ServerInfoService.name, () => {
it('should respond the server features', async () => { it('should respond the server features', async () => {
await expect(sut.getFeatures()).resolves.toEqual({ await expect(sut.getFeatures()).resolves.toEqual({
smartSearch: true, smartSearch: true,
duplicateDetection: true, duplicateDetection: false,
facialRecognition: true, facialRecognition: true,
map: true, map: true,
reverseGeocoding: true, reverseGeocoding: true,
@@ -81,8 +81,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
modelName: 'ViT-B-32__openai', modelName: 'ViT-B-32__openai',
}, },
duplicateDetection: { duplicateDetection: {
enabled: true, enabled: false,
maxDistance: 0.0155, maxDistance: 0.03,
}, },
facialRecognition: { facialRecognition: {
enabled: true, enabled: true,
@@ -1,197 +0,0 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { mapUserAdmin } from 'src/dtos/user.dto';
import { UserStatus } from 'src/entities/user.entity';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { UserAdminService } from 'src/services/user-admin.service';
import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
import { Mocked, describe } from 'vitest';
describe(UserAdminService.name, () => {
let sut: UserAdminService;
let userMock: Mocked<IUserRepository>;
let cryptoRepositoryMock: Mocked<ICryptoRepository>;
let albumMock: Mocked<IAlbumRepository>;
let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggerRepository>;
beforeEach(() => {
albumMock = newAlbumRepositoryMock();
cryptoRepositoryMock = newCryptoRepositoryMock();
jobMock = newJobRepositoryMock();
userMock = newUserRepositoryMock();
loggerMock = newLoggerRepositoryMock();
sut = new UserAdminService(albumMock, cryptoRepositoryMock, jobMock, userMock, loggerMock);
userMock.get.mockImplementation((userId) =>
Promise.resolve([userStub.admin, userStub.user1].find((user) => user.id === userId) ?? null),
);
});
describe('create', () => {
it('should not create a user if there is no local admin account', async () => {
userMock.getAdmin.mockResolvedValueOnce(null);
await expect(
sut.create({
email: 'john_smith@email.com',
name: 'John Smith',
password: 'password',
}),
).rejects.toBeInstanceOf(BadRequestException);
});
it('should create user', async () => {
userMock.getAdmin.mockResolvedValue(userStub.admin);
userMock.create.mockResolvedValue(userStub.user1);
await expect(
sut.create({
email: userStub.user1.email,
name: userStub.user1.name,
password: 'password',
storageLabel: 'label',
}),
).resolves.toEqual(mapUserAdmin(userStub.user1));
expect(userMock.getAdmin).toBeCalled();
expect(userMock.create).toBeCalledWith({
email: userStub.user1.email,
name: userStub.user1.name,
storageLabel: 'label',
password: expect.anything(),
});
});
});
describe('update', () => {
it('should update the user', async () => {
const update = {
shouldChangePassword: true,
email: 'immich@test.com',
storageLabel: 'storage_label',
};
userMock.getByEmail.mockResolvedValue(null);
userMock.getByStorageLabel.mockResolvedValue(null);
userMock.update.mockResolvedValue(userStub.user1);
await sut.update(authStub.user1, userStub.user1.id, update);
expect(userMock.getByEmail).toHaveBeenCalledWith(update.email);
expect(userMock.getByStorageLabel).toHaveBeenCalledWith(update.storageLabel);
});
it('should not set an empty string for storage label', async () => {
userMock.update.mockResolvedValue(userStub.user1);
await sut.update(authStub.admin, userStub.user1.id, { storageLabel: '' });
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
storageLabel: null,
updatedAt: expect.any(Date),
});
});
it('should not change an email to one already in use', async () => {
const dto = { id: userStub.user1.id, email: 'updated@test.com' };
userMock.get.mockResolvedValue(userStub.user1);
userMock.getByEmail.mockResolvedValue(userStub.admin);
await expect(sut.update(authStub.admin, userStub.user1.id, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.update).not.toHaveBeenCalled();
});
it('should not let the admin change the storage label to one already in use', async () => {
const dto = { id: userStub.user1.id, storageLabel: 'admin' };
userMock.get.mockResolvedValue(userStub.user1);
userMock.getByStorageLabel.mockResolvedValue(userStub.admin);
await expect(sut.update(authStub.admin, userStub.user1.id, dto)).rejects.toBeInstanceOf(BadRequestException);
expect(userMock.update).not.toHaveBeenCalled();
});
it('update user information should throw error if user not found', async () => {
userMock.get.mockResolvedValueOnce(null);
await expect(
sut.update(authStub.admin, userStub.user1.id, { shouldChangePassword: true }),
).rejects.toBeInstanceOf(BadRequestException);
});
});
describe('delete', () => {
it('should throw error if user could not be found', async () => {
userMock.get.mockResolvedValue(null);
await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toThrowError(BadRequestException);
expect(userMock.delete).not.toHaveBeenCalled();
});
it('cannot delete admin user', async () => {
await expect(sut.delete(authStub.admin, userStub.admin.id, {})).rejects.toBeInstanceOf(ForbiddenException);
});
it('should require the auth user be an admin', async () => {
await expect(sut.delete(authStub.user1, authStub.admin.user.id, {})).rejects.toBeInstanceOf(ForbiddenException);
expect(userMock.delete).not.toHaveBeenCalled();
});
it('should delete user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.delete(authStub.admin, userStub.user1.id, {})).resolves.toEqual(mapUserAdmin(userStub.user1));
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
status: UserStatus.DELETED,
deletedAt: expect.any(Date),
});
});
it('should force delete user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.delete(authStub.admin, userStub.user1.id, { force: true })).resolves.toEqual(
mapUserAdmin(userStub.user1),
);
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
status: UserStatus.REMOVING,
deletedAt: expect.any(Date),
});
expect(jobMock.queue).toHaveBeenCalledWith({
name: JobName.USER_DELETION,
data: { id: userStub.user1.id, force: true },
});
});
});
describe('restore', () => {
it('should throw error if user could not be found', async () => {
userMock.get.mockResolvedValue(null);
await expect(sut.restore(authStub.admin, userStub.admin.id)).rejects.toThrowError(BadRequestException);
expect(userMock.update).not.toHaveBeenCalled();
});
it('should restore an user', async () => {
userMock.get.mockResolvedValue(userStub.user1);
userMock.update.mockResolvedValue(userStub.user1);
await expect(sut.restore(authStub.admin, userStub.user1.id)).resolves.toEqual(mapUserAdmin(userStub.user1));
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { status: UserStatus.ACTIVE, deletedAt: null });
});
});
});

Some files were not shown because too many files have changed in this diff Show More