mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
refactor(server): user endpoints (#9730)
* refactor(server): user endpoints * fix repos * fix unit tests --------- Co-authored-by: Daniel Dietzler <mail@ddietzler.dev> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
e7c8501930
commit
75830a4878
80 changed files with 1696 additions and 1341 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { getMyUserInfo } from '@immich/sdk';
|
import { getMyUser } 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, userInfo] = await withError(getMyUserInfo());
|
const [error, user] = await withError(getMyUser());
|
||||||
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 ${userInfo.email}`);
|
console.log(`Logged in as ${user.email}`);
|
||||||
|
|
||||||
if (!existsSync(configDir)) {
|
if (!existsSync(configDir)) {
|
||||||
// Create config folder if it doesn't exist
|
// Create config folder if it doesn't exist
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
import { getAssetStatistics, getMyUser, 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({}),
|
||||||
getMyUserInfo(),
|
getMyUser(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(`Server Info (via ${userInfo.email})`);
|
console.log(`Server Info (via ${userInfo.email})`);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getMyUserInfo, init, isHttpError } from '@immich/sdk';
|
import { getMyUser, 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(getMyUserInfo());
|
const [error] = await withError(getMyUser());
|
||||||
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);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
AssetFileUploadResponseDto,
|
AssetFileUploadResponseDto,
|
||||||
AssetOrder,
|
AssetOrder,
|
||||||
deleteUser,
|
deleteUserAdmin,
|
||||||
getAlbumInfo,
|
getAlbumInfo,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
|
@ -107,7 +107,7 @@ describe('/albums', () => {
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /albums', () => {
|
describe('GET /albums', () => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
getMyUserInfo,
|
getMyUser,
|
||||||
updateAssets,
|
updateAssets,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { exiftool } from 'exiftool-vendored';
|
import { exiftool } from 'exiftool-vendored';
|
||||||
|
@ -1162,7 +1162,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 getMyUserInfo({ headers: asBearerAuth(quotaUser.accessToken) });
|
const user = await getMyUser({ headers: asBearerAuth(quotaUser.accessToken) });
|
||||||
|
|
||||||
expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
|
expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
deleteUser,
|
deleteUserAdmin,
|
||||||
} 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 deleteUser({ id: user2.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /share/${key}', () => {
|
describe('GET /share/${key}', () => {
|
||||||
|
|
317
e2e/src/api/specs/user-admin.e2e-spec.ts
Normal file
317
e2e/src/api/specs/user-admin.e2e-spec.ts
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,37 +1,28 @@
|
||||||
import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
|
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyUser, login } from '@immich/sdk';
|
||||||
import { Socket } from 'socket.io-client';
|
import { createUserDto } from 'src/fixtures';
|
||||||
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 { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { 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 });
|
||||||
|
|
||||||
[websocket, deletedUser, nonAdmin, userToDelete, userToHardDelete] = await Promise.all([
|
[deletedUser, nonAdmin] = 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 deleteUser({ id: deletedUser.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUserAdmin(
|
||||||
});
|
{ id: deletedUser.userId, userAdminDeleteDto: {} },
|
||||||
|
{ headers: asBearerAuth(admin.accessToken) },
|
||||||
afterAll(() => {
|
);
|
||||||
utils.disconnectWebsocket(websocket);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /users', () => {
|
describe('GET /users', () => {
|
||||||
|
@ -44,71 +35,14 @@ 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(5);
|
expect(body).toHaveLength(2);
|
||||||
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', () => {
|
||||||
|
@ -118,154 +52,54 @@ describe('/users', () => {
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get my info', async () => {
|
it('should not work for shared links', 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('POST /users', () => {
|
describe('PUT /users/me', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).post(`/users`).send(createUserDto.user1);
|
const { status, body } = await request(app).put(`/users/me`);
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const key of Object.keys(createUserDto.user1)) {
|
for (const key of ['email', 'name', 'memoriesEnabled', 'avatarColor']) {
|
||||||
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)
|
||||||
.post(`/users`)
|
.put(`/users/me`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...createUserDto.user1, [key]: null });
|
.send(dto);
|
||||||
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 getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/users`)
|
.put(`/users/me`)
|
||||||
.send({
|
.send({ name: 'Name' })
|
||||||
id: admin.userId,
|
|
||||||
name: 'Name',
|
|
||||||
})
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
|
@ -274,17 +108,13 @@ 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 getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/users`)
|
.put(`/users/me`)
|
||||||
.send({
|
.send({ memoriesEnabled: false })
|
||||||
id: admin.userId,
|
|
||||||
memoriesEnabled: false,
|
|
||||||
})
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
|
@ -293,7 +123,80 @@ 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(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,10 +5,10 @@ import {
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateAssetDto,
|
CreateAssetDto,
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
CreateUserDto,
|
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
|
UserAdminCreateDto,
|
||||||
ValidateLibraryDto,
|
ValidateLibraryDto,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
|
@ -16,7 +16,7 @@ import {
|
||||||
createPartner,
|
createPartner,
|
||||||
createPerson,
|
createPerson,
|
||||||
createSharedLink,
|
createSharedLink,
|
||||||
createUser,
|
createUserAdmin,
|
||||||
deleteAssets,
|
deleteAssets,
|
||||||
getAllJobsStatus,
|
getAllJobsStatus,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
|
@ -273,8 +273,8 @@ export const utils = {
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
userSetup: async (accessToken: string, dto: UserAdminCreateDto) => {
|
||||||
await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
|
await createUserAdmin({ userAdminCreateDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||||
return login({
|
return login({
|
||||||
loginCredentialDto: { email: dto.email, password: dto.password },
|
loginCredentialDto: { email: dto.email, password: dto.password },
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,7 @@ class User {
|
||||||
|
|
||||||
Id get isarId => fastHash(id);
|
Id get isarId => fastHash(id);
|
||||||
|
|
||||||
User.fromUserDto(UserResponseDto dto)
|
User.fromUserDto(UserAdminResponseDto 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 = dto.updatedAt,
|
updatedAt = DateTime.now(),
|
||||||
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 = dto.isAdmin,
|
isAdmin = false,
|
||||||
memoryEnabled = dto.memoriesEnabled ?? false,
|
memoryEnabled = false,
|
||||||
avatarColor = dto.avatarColor.toAvatarColor(),
|
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||||
inTimeline = dto.inTimeline ?? false,
|
inTimeline = dto.inTimeline ?? false,
|
||||||
quotaUsageInBytes = dto.quotaUsageInBytes ?? 0,
|
quotaUsageInBytes = 0,
|
||||||
quotaSizeInBytes = dto.quotaSizeInBytes ?? 0;
|
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(UserDto dto)
|
User.fromSimpleUserDto(UserResponseDto dto)
|
||||||
: id = dto.id,
|
: id = dto.id,
|
||||||
email = dto.email,
|
email = dto.email,
|
||||||
name = dto.name,
|
name = dto.name,
|
||||||
|
|
|
@ -138,11 +138,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
|
|
||||||
Future<bool> changePassword(String newPassword) async {
|
Future<bool> changePassword(String newPassword) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.userApi.updateUser(
|
await _apiService.userApi.updateMyUser(
|
||||||
UpdateUserDto(
|
UserUpdateMeDto(
|
||||||
id: state.userId,
|
|
||||||
password: newPassword,
|
password: newPassword,
|
||||||
shouldChangePassword: false,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -178,9 +176,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
user = offlineUser;
|
user = offlineUser;
|
||||||
retResult = false;
|
retResult = false;
|
||||||
} else {
|
} else {
|
||||||
UserResponseDto? userResponseDto;
|
UserAdminResponseDto? userResponseDto;
|
||||||
try {
|
try {
|
||||||
userResponseDto = await _apiService.userApi.getMyUserInfo();
|
userResponseDto = await _apiService.userApi.getMyUser();
|
||||||
} 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]",
|
||||||
|
|
|
@ -20,7 +20,7 @@ class CurrentUserProvider extends StateNotifier<User?> {
|
||||||
|
|
||||||
refresh() async {
|
refresh() async {
|
||||||
try {
|
try {
|
||||||
final user = await _apiService.userApi.getMyUserInfo();
|
final user = await _apiService.userApi.getMyUser();
|
||||||
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.getMyUserInfo();
|
await ref.read(apiServiceProvider).userApi.getMyUser();
|
||||||
|
|
||||||
if (userResponseDto == null) {
|
if (userResponseDto == null) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -37,10 +37,10 @@ class UserService {
|
||||||
this._partnerService,
|
this._partnerService,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<User>?> _getAllUsers({required bool isAll}) async {
|
Future<List<User>?> _getAllUsers() async {
|
||||||
try {
|
try {
|
||||||
final dto = await _apiService.userApi.getAllUsers(isAll);
|
final dto = await _apiService.userApi.searchUsers();
|
||||||
return dto?.map(User.fromUserDto).toList();
|
return dto?.map(User.fromSimpleUserDto).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(isAll: true);
|
final List<User>? users = await _getAllUsers();
|
||||||
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 =
|
||||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/authentication_api.dart
generated
BIN
mobile/openapi/lib/api/authentication_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/o_auth_api.dart
generated
BIN
mobile/openapi/lib/api/o_auth_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/user_api.dart
generated
BIN
mobile/openapi/lib/api/user_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/activity_response_dto.dart
generated
BIN
mobile/openapi/lib/model/activity_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/partner_response_dto.dart
generated
BIN
mobile/openapi/lib/model/partner_response_dto.dart
generated
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/user_admin_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/user_admin_response_dto.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/user_dto.dart
generated
BIN
mobile/openapi/lib/model/user_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/user_response_dto.dart
generated
BIN
mobile/openapi/lib/model/user_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/user_update_me_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/user_update_me_dto.dart
generated
Normal file
Binary file not shown.
|
@ -206,6 +206,274 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/users": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "searchUsersAdmin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "withDeleted",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "createUserAdmin",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminCreateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/users/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deleteUserAdmin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminDeleteDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "getUserAdmin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "updateUserAdmin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminUpdateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/users/{id}/restore": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "restoreUserAdmin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"User"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/albums": {
|
"/albums": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAllAlbums",
|
"operationId": "getAllAlbums",
|
||||||
|
@ -1879,7 +2147,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1910,7 +2178,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3160,7 +3428,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3206,7 +3474,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6030,17 +6298,8 @@
|
||||||
},
|
},
|
||||||
"/users": {
|
"/users": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAllUsers",
|
"operationId": "searchUsers",
|
||||||
"parameters": [
|
"parameters": [],
|
||||||
{
|
|
||||||
"name": "isAll",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"content": {
|
"content": {
|
||||||
|
@ -6070,26 +6329,18 @@
|
||||||
"tags": [
|
"tags": [
|
||||||
"User"
|
"User"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
"post": {
|
},
|
||||||
"operationId": "createUser",
|
"/users/me": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getMyUser",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/CreateUserDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"201": {
|
"200": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6112,13 +6363,13 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"put": {
|
"put": {
|
||||||
"operationId": "updateUser",
|
"operationId": "updateMyUser",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UpdateUserDto"
|
"$ref": "#/components/schemas/UserUpdateMeDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6129,39 +6380,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
"$ref": "#/components/schemas/UserAdminResponseDto"
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"User"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/users/me": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getMyUserInfo",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6251,58 +6470,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/users/{id}": {
|
"/users/{id}": {
|
||||||
"delete": {
|
|
||||||
"operationId": "deleteUser",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/DeleteUserDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"User"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getUserById",
|
"operationId": "getUser",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
|
@ -6384,48 +6553,6 @@
|
||||||
"User"
|
"User"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"/users/{id}/restore": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "restoreUser",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"User"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
|
@ -6567,7 +6694,7 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"$ref": "#/components/schemas/UserDto"
|
"$ref": "#/components/schemas/UserResponseDto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -7775,52 +7902,6 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"CreateUserDto": {
|
|
||||||
"properties": {
|
|
||||||
"email": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"memoriesEnabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"notify": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"quotaSizeInBytes": {
|
|
||||||
"format": "int64",
|
|
||||||
"minimum": 1,
|
|
||||||
"nullable": true,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"shouldChangePassword": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"storageLabel": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"email",
|
|
||||||
"name",
|
|
||||||
"password"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"DeleteUserDto": {
|
|
||||||
"properties": {
|
|
||||||
"force": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"DownloadArchiveInfo": {
|
"DownloadArchiveInfo": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"assetIds": {
|
"assetIds": {
|
||||||
|
@ -8803,15 +8884,6 @@
|
||||||
"avatarColor": {
|
"avatarColor": {
|
||||||
"$ref": "#/components/schemas/UserAvatarColor"
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
},
|
},
|
||||||
"createdAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -8821,62 +8893,19 @@
|
||||||
"inTimeline": {
|
"inTimeline": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"isAdmin": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"memoriesEnabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"oauthId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"profileImagePath": {
|
"profileImagePath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
|
||||||
"quotaSizeInBytes": {
|
|
||||||
"format": "int64",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"quotaUsageInBytes": {
|
|
||||||
"format": "int64",
|
|
||||||
"nullable": true,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"shouldChangePassword": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"$ref": "#/components/schemas/UserStatus"
|
|
||||||
},
|
|
||||||
"storageLabel": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"avatarColor",
|
"avatarColor",
|
||||||
"createdAt",
|
|
||||||
"deletedAt",
|
|
||||||
"email",
|
"email",
|
||||||
"id",
|
"id",
|
||||||
"isAdmin",
|
|
||||||
"name",
|
"name",
|
||||||
"oauthId",
|
"profileImagePath"
|
||||||
"profileImagePath",
|
|
||||||
"quotaSizeInBytes",
|
|
||||||
"quotaUsageInBytes",
|
|
||||||
"shouldChangePassword",
|
|
||||||
"status",
|
|
||||||
"storageLabel",
|
|
||||||
"updatedAt"
|
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
@ -10810,48 +10839,6 @@
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"UpdateUserDto": {
|
|
||||||
"properties": {
|
|
||||||
"avatarColor": {
|
|
||||||
"$ref": "#/components/schemas/UserAvatarColor"
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"isAdmin": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"memoriesEnabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"quotaSizeInBytes": {
|
|
||||||
"format": "int64",
|
|
||||||
"minimum": 1,
|
|
||||||
"nullable": true,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"shouldChangePassword": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"storageLabel": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"UsageByUserDto": {
|
"UsageByUserDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"photos": {
|
"photos": {
|
||||||
|
@ -10886,49 +10873,53 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"UserAvatarColor": {
|
"UserAdminCreateDto": {
|
||||||
"enum": [
|
|
||||||
"primary",
|
|
||||||
"pink",
|
|
||||||
"red",
|
|
||||||
"yellow",
|
|
||||||
"blue",
|
|
||||||
"green",
|
|
||||||
"purple",
|
|
||||||
"orange",
|
|
||||||
"gray",
|
|
||||||
"amber"
|
|
||||||
],
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"UserDto": {
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatarColor": {
|
|
||||||
"$ref": "#/components/schemas/UserAvatarColor"
|
|
||||||
},
|
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"id": {
|
"memoriesEnabled": {
|
||||||
"type": "string"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"profileImagePath": {
|
"notify": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"quotaSizeInBytes": {
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 1,
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"shouldChangePassword": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"storageLabel": {
|
||||||
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"avatarColor",
|
|
||||||
"email",
|
"email",
|
||||||
"id",
|
|
||||||
"name",
|
"name",
|
||||||
"profileImagePath"
|
"password"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"UserResponseDto": {
|
"UserAdminDeleteDto": {
|
||||||
|
"properties": {
|
||||||
|
"force": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"UserAdminResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatarColor": {
|
"avatarColor": {
|
||||||
"$ref": "#/components/schemas/UserAvatarColor"
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
@ -11007,6 +10998,81 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"UserAdminUpdateDto": {
|
||||||
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"memoriesEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"quotaSizeInBytes": {
|
||||||
|
"format": "int64",
|
||||||
|
"minimum": 1,
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"shouldChangePassword": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"storageLabel": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"UserAvatarColor": {
|
||||||
|
"enum": [
|
||||||
|
"primary",
|
||||||
|
"pink",
|
||||||
|
"red",
|
||||||
|
"yellow",
|
||||||
|
"blue",
|
||||||
|
"green",
|
||||||
|
"purple",
|
||||||
|
"orange",
|
||||||
|
"gray",
|
||||||
|
"amber"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"UserResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"profileImagePath": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"avatarColor",
|
||||||
|
"email",
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"profileImagePath"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"UserStatus": {
|
"UserStatus": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"active",
|
"active",
|
||||||
|
@ -11015,6 +11081,26 @@
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"UserUpdateMeDto": {
|
||||||
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"memoriesEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ValidateAccessTokenResponseDto": {
|
"ValidateAccessTokenResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"authStatus": {
|
"authStatus": {
|
||||||
|
|
|
@ -13,13 +13,22 @@ 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 });
|
||||||
|
|
|
@ -14,7 +14,7 @@ const oazapfts = Oazapfts.runtime(defaults);
|
||||||
export const servers = {
|
export const servers = {
|
||||||
server1: "/api"
|
server1: "/api"
|
||||||
};
|
};
|
||||||
export type UserDto = {
|
export type UserResponseDto = {
|
||||||
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: UserDto;
|
user: UserResponseDto;
|
||||||
};
|
};
|
||||||
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 UserResponseDto = {
|
export type UserAdminResponseDto = {
|
||||||
avatarColor: UserAvatarColor;
|
avatarColor: UserAvatarColor;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
|
@ -56,6 +56,29 @@ export type UserResponseDto = {
|
||||||
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;
|
||||||
|
@ -517,22 +540,11 @@ 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;
|
||||||
|
@ -1060,27 +1072,12 @@ export type TimeBucketResponseDto = {
|
||||||
count: number;
|
count: number;
|
||||||
timeBucket: string;
|
timeBucket: string;
|
||||||
};
|
};
|
||||||
export type CreateUserDto = {
|
export type UserUpdateMeDto = {
|
||||||
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;
|
||||||
|
@ -1089,9 +1086,6 @@ 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;
|
||||||
|
@ -1146,6 +1140,77 @@ 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;
|
||||||
|
@ -1589,7 +1654,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: UserResponseDto;
|
data: UserAdminResponseDto;
|
||||||
}>("/auth/admin-sign-up", oazapfts.json({
|
}>("/auth/admin-sign-up", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -1601,7 +1666,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: UserResponseDto;
|
data: UserAdminResponseDto;
|
||||||
}>("/auth/change-password", oazapfts.json({
|
}>("/auth/change-password", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -1934,7 +1999,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: UserResponseDto;
|
data: UserAdminResponseDto;
|
||||||
}>("/oauth/link", oazapfts.json({
|
}>("/oauth/link", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -1949,7 +2014,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: UserResponseDto;
|
data: UserAdminResponseDto;
|
||||||
}>("/oauth/unlink", {
|
}>("/oauth/unlink", {
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST"
|
method: "POST"
|
||||||
|
@ -2687,50 +2752,34 @@ export function restoreAssets({ bulkIdsDto }: {
|
||||||
body: bulkIdsDto
|
body: bulkIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getAllUsers({ isAll }: {
|
export function searchUsers(opts?: Oazapfts.RequestOpts) {
|
||||||
isAll: boolean;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: UserResponseDto[];
|
data: UserResponseDto[];
|
||||||
}>(`/users${QS.query(QS.explode({
|
}>("/users", {
|
||||||
isAll
|
|
||||||
}))}`, {
|
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function createUser({ createUserDto }: {
|
export function getMyUser(opts?: Oazapfts.RequestOpts) {
|
||||||
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<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: UserResponseDto;
|
data: UserAdminResponseDto;
|
||||||
}>("/users", oazapfts.json({
|
|
||||||
...opts,
|
|
||||||
method: "PUT",
|
|
||||||
body: updateUserDto
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
export function getMyUserInfo(opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: UserResponseDto;
|
|
||||||
}>("/users/me", {
|
}>("/users/me", {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function updateMyUser({ userUpdateMeDto }: {
|
||||||
|
userUpdateMeDto: UserUpdateMeDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: UserAdminResponseDto;
|
||||||
|
}>("/users/me", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "PUT",
|
||||||
|
body: userUpdateMeDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
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,
|
||||||
|
@ -2749,20 +2798,7 @@ export function createProfileImage({ createProfileImageDto }: {
|
||||||
body: createProfileImageDto
|
body: createProfileImageDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function deleteUser({ id, deleteUserDto }: {
|
export function getUser({ id }: {
|
||||||
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<{
|
||||||
|
@ -2782,17 +2818,6 @@ 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"
|
||||||
|
@ -2817,15 +2842,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",
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
|
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
|
||||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto } 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: UserResponseDto) {
|
return function ask(admin: UserAdminResponseDto) {
|
||||||
const { id, oauthId, email, name } = admin;
|
const { id, oauthId, email, name } = admin;
|
||||||
console.log(`Found Admin:
|
console.log(`Found Admin:
|
||||||
- ID=${id}
|
- ID=${id}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
ValidateAccessTokenResponseDto,
|
ValidateAccessTokenResponseDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto } 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<UserResponseDto> {
|
signUpAdmin(@Body() dto: SignUpDto): Promise<UserAdminResponseDto> {
|
||||||
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<UserResponseDto> {
|
changePassword(@Auth() auth: AuthDto, @Body() dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
||||||
return this.service.changePassword(auth, dto).then(mapUser);
|
return this.service.changePassword(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('logout')
|
@Post('logout')
|
||||||
|
|
|
@ -27,6 +27,7 @@ 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 = [
|
||||||
|
@ -59,5 +60,6 @@ export const controllers = [
|
||||||
TagController,
|
TagController,
|
||||||
TimelineController,
|
TimelineController,
|
||||||
TrashController,
|
TrashController,
|
||||||
|
UserAdminController,
|
||||||
UserController,
|
UserController,
|
||||||
];
|
];
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
OAuthCallbackDto,
|
OAuthCallbackDto,
|
||||||
OAuthConfigDto,
|
OAuthConfigDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto } 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<UserResponseDto> {
|
linkOAuthAccount(@Auth() auth: AuthDto, @Body() dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
|
||||||
return this.service.link(auth, dto);
|
return this.service.link(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('unlink')
|
@Post('unlink')
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserResponseDto> {
|
unlinkOAuthAccount(@Auth() auth: AuthDto): Promise<UserAdminResponseDto> {
|
||||||
return this.service.unlink(auth);
|
return this.service.unlink(auth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
63
server/src/controllers/user-admin.controller.ts
Normal file
63
server/src/controllers/user-admin.controller.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import {
|
||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
|
||||||
Res,
|
Res,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
|
@ -19,7 +18,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 { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } 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';
|
||||||
|
@ -37,58 +36,28 @@ export class UserController {
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
getAllUsers(@Auth() auth: AuthDto, @Query('isAll') isAll: boolean): Promise<UserResponseDto[]> {
|
searchUsers(): Promise<UserResponseDto[]> {
|
||||||
return this.service.getAll(auth, isAll);
|
return this.service.search();
|
||||||
}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
@Authenticated({ admin: true })
|
|
||||||
createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
|
||||||
return this.service.create(createUserDto);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('me')
|
@Get('me')
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
getMyUserInfo(@Auth() auth: AuthDto): Promise<UserResponseDto> {
|
getMyUser(@Auth() auth: AuthDto): UserAdminResponseDto {
|
||||||
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()
|
||||||
getUserById(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
getUser(@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 })
|
||||||
|
@ -101,6 +70,13 @@ 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()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException } 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';
|
||||||
|
@ -26,46 +25,6 @@ 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) {
|
||||||
|
|
|
@ -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 { UserDto, mapSimpleUser } from 'src/dtos/user.dto';
|
import { UserResponseDto, mapUser } 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!: UserDto;
|
user!: UserResponseDto;
|
||||||
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: mapSimpleUser(activity.user),
|
user: mapUser(activity.user),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto';
|
import { UserAdminCreateDto, UserUpdateMeDto } 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(UpdateUserDto, {
|
const dto = plainToInstance(UserUpdateMeDto, {
|
||||||
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<CreateUserDto> = {
|
const params: Partial<UserAdminCreateDto> = {
|
||||||
email: undefined,
|
email: undefined,
|
||||||
password: 'password',
|
password: 'password',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
};
|
};
|
||||||
let dto: CreateUserDto = plainToInstance(CreateUserDto, params);
|
let dto: UserAdminCreateDto = plainToInstance(UserAdminCreateDto, 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(CreateUserDto, params);
|
dto = plainToInstance(UserAdminCreateDto, 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(CreateUserDto, params);
|
dto = plainToInstance(UserAdminCreateDto, 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(CreateUserDto, {
|
const dto = plainToInstance(UserAdminCreateDto, {
|
||||||
email: someEmail,
|
email: someEmail,
|
||||||
password: 'some password',
|
password: 'some password',
|
||||||
name: 'some name',
|
name: 'some name',
|
||||||
|
@ -51,18 +51,3 @@ 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,12 +1,63 @@
|
||||||
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, IsUUID } from 'class-validator';
|
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString } 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 CreateUserDto {
|
export class UserUpdateMeDto {
|
||||||
|
@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;
|
||||||
|
@ -41,23 +92,7 @@ export class CreateUserDto {
|
||||||
notify?: boolean;
|
notify?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateUserOAuthDto {
|
export class UserAdminUpdateDto {
|
||||||
@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)
|
||||||
|
@ -73,18 +108,10 @@ export class UpdateUserDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@Optional()
|
@Optional({ nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
storageLabel?: string;
|
storageLabel?: string | null;
|
||||||
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsUUID('4')
|
|
||||||
@ApiProperty({ format: 'uuid' })
|
|
||||||
id!: string;
|
|
||||||
|
|
||||||
@ValidateBoolean({ optional: true })
|
|
||||||
isAdmin?: boolean;
|
|
||||||
|
|
||||||
@ValidateBoolean({ optional: true })
|
@ValidateBoolean({ optional: true })
|
||||||
shouldChangePassword?: boolean;
|
shouldChangePassword?: boolean;
|
||||||
|
@ -104,17 +131,12 @@ export class UpdateUserDto {
|
||||||
quotaSizeInBytes?: number | null;
|
quotaSizeInBytes?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserDto {
|
export class UserAdminDeleteDto {
|
||||||
id!: string;
|
@ValidateBoolean({ optional: true })
|
||||||
name!: string;
|
force?: boolean;
|
||||||
email!: string;
|
|
||||||
profileImagePath!: string;
|
|
||||||
@IsEnum(UserAvatarColor)
|
|
||||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
|
||||||
avatarColor!: UserAvatarColor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserResponseDto extends UserDto {
|
export class UserAdminResponseDto extends UserResponseDto {
|
||||||
storageLabel!: string | null;
|
storageLabel!: string | null;
|
||||||
shouldChangePassword!: boolean;
|
shouldChangePassword!: boolean;
|
||||||
isAdmin!: boolean;
|
isAdmin!: boolean;
|
||||||
|
@ -131,19 +153,9 @@ export class UserResponseDto extends UserDto {
|
||||||
status!: string;
|
status!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapSimpleUser = (entity: UserEntity): UserDto => {
|
export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto {
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
...mapUser(entity),
|
||||||
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,
|
||||||
|
|
|
@ -22,13 +22,17 @@ 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"
|
||||||
|
|
|
@ -38,13 +38,17 @@ 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"
|
||||||
|
|
|
@ -34,7 +34,9 @@ export class ApiKeyRepository implements IKeyRepository {
|
||||||
},
|
},
|
||||||
where: { key: hashedToken },
|
where: { key: hashedToken },
|
||||||
relations: {
|
relations: {
|
||||||
user: true,
|
user: {
|
||||||
|
metadata: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,14 @@ 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({ where: { token }, relations: { user: true } });
|
return this.repository.findOne({
|
||||||
|
where: { token },
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
metadata: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getByUserId(userId: string): Promise<SessionEntity[]> {
|
getByUserId(userId: string): Promise<SessionEntity[]> {
|
||||||
|
|
|
@ -138,6 +138,7 @@ 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);
|
||||||
|
|
||||||
|
|
|
@ -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 } from 'src/constants';
|
import { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } 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 { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, mapUserAdmin } 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) {
|
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
||||||
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,10 +121,14 @@ export class AuthService {
|
||||||
throw new BadRequestException('Wrong password');
|
throw new BadRequestException('Wrong password');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.userCore.updateUser(auth.user, auth.user.id, { password: newPassword });
|
const hashedPassword = await this.cryptoRepository.hashBcrypt(newPassword, SALT_ROUNDS);
|
||||||
|
|
||||||
|
const updatedUser = await this.userRepository.update(user.id, { password: hashedPassword });
|
||||||
|
|
||||||
|
return mapUserAdmin(updatedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async adminSignUp(dto: SignUpDto): Promise<UserResponseDto> {
|
async adminSignUp(dto: SignUpDto): Promise<UserAdminResponseDto> {
|
||||||
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');
|
||||||
|
@ -138,7 +142,7 @@ export class AuthService {
|
||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapUser(admin);
|
return mapUserAdmin(admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
|
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
|
||||||
|
@ -237,7 +241,7 @@ export class AuthService {
|
||||||
return this.createLoginResponse(user, loginDetails);
|
return this.createLoginResponse(user, loginDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
|
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
|
||||||
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);
|
||||||
|
@ -245,11 +249,14 @@ 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<UserResponseDto> {
|
async unlink(auth: AuthDto): Promise<UserAdminResponseDto> {
|
||||||
return mapUser(await this.userRepository.update(auth.user.id, { oauthId: '' }));
|
const user = 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> {
|
||||||
|
|
|
@ -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 { UserCore } from 'src/cores/user.core';
|
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
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,7 +10,6 @@ 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,
|
||||||
|
@ -18,26 +17,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<UserResponseDto[]> {
|
async listUsers(): Promise<UserAdminResponseDto[]> {
|
||||||
const users = await this.userRepository.getList({ withDeleted: true });
|
const users = await this.userRepository.getList({ withDeleted: true });
|
||||||
return users.map((user) => mapUser(user));
|
return users.map((user) => mapUserAdmin(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetAdminPassword(ask: (admin: UserResponseDto) => Promise<string | undefined>) {
|
async resetAdminPassword(ask: (admin: UserAdminResponseDto) => 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(mapUser(admin));
|
const providedPassword = await ask(mapUserAdmin(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.userCore.updateUser(admin, admin.id, { password });
|
await this.userRepository.update(admin.id, { password: hashedPassword });
|
||||||
|
|
||||||
return { admin, password, provided: !!providedPassword };
|
return { admin, password, provided: !!providedPassword };
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ 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';
|
||||||
|
|
||||||
|
@ -73,5 +74,6 @@ export const services = [
|
||||||
TimelineService,
|
TimelineService,
|
||||||
TrashService,
|
TrashService,
|
||||||
UserService,
|
UserService,
|
||||||
|
UserAdminService,
|
||||||
VersionService,
|
VersionService,
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
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';
|
||||||
|
@ -9,45 +7,6 @@ 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>;
|
||||||
|
@ -65,13 +24,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.toEqual([responseDto.admin]);
|
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toBeDefined();
|
||||||
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.toEqual([responseDto.admin]);
|
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toBeDefined();
|
||||||
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -81,7 +40,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.toEqual(responseDto.user1);
|
await expect(sut.create(authStub.admin, authStub.user1.user.id)).resolves.toBeDefined();
|
||||||
|
|
||||||
expect(partnerMock.create).toHaveBeenCalledWith({
|
expect(partnerMock.create).toHaveBeenCalledWith({
|
||||||
sharedById: authStub.admin.user.id,
|
sharedById: authStub.admin.user.id,
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class PartnerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const partner = await this.repository.create(partnerId);
|
const partner = await this.repository.create(partnerId);
|
||||||
return this.mapToPartnerEntity(partner, PartnerDirection.SharedBy);
|
return this.mapPartner(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.mapToPartnerEntity(partner, direction));
|
.map((partner) => this.mapPartner(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.mapToPartnerEntity(entity, PartnerDirection.SharedWith);
|
return this.mapPartner(entity, PartnerDirection.SharedWith);
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapToPartnerEntity(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto {
|
private mapPartner(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,
|
||||||
|
|
197
server/src/services/user-admin.service.spec.ts
Normal file
197
server/src/services/user-admin.service.spec.ts
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
154
server/src/services/user-admin.service.ts
Normal file
154
server/src/services/user-admin.service.ts
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
|
import { UserCore } from 'src/cores/user.core';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import {
|
||||||
|
UserAdminCreateDto,
|
||||||
|
UserAdminDeleteDto,
|
||||||
|
UserAdminResponseDto,
|
||||||
|
UserAdminSearchDto,
|
||||||
|
UserAdminUpdateDto,
|
||||||
|
mapUserAdmin,
|
||||||
|
} from 'src/dtos/user.dto';
|
||||||
|
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||||
|
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, UserFindOptions } from 'src/interfaces/user.interface';
|
||||||
|
import { getPreferences, getPreferencesPartial } from 'src/utils/preferences';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserAdminService {
|
||||||
|
private userCore: UserCore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
|
) {
|
||||||
|
this.userCore = UserCore.create(cryptoRepository, userRepository);
|
||||||
|
this.logger.setContext(UserAdminService.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(auth: AuthDto, dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
|
||||||
|
const users = await this.userRepository.getList({ withDeleted: dto.withDeleted });
|
||||||
|
return users.map((user) => mapUserAdmin(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
|
||||||
|
const { memoriesEnabled, notify, ...rest } = dto;
|
||||||
|
let user = await this.userCore.createUser(rest);
|
||||||
|
|
||||||
|
// TODO remove and replace with entire dto.preferences config
|
||||||
|
if (memoriesEnabled === false) {
|
||||||
|
await this.userRepository.upsertMetadata(user.id, {
|
||||||
|
key: UserMetadataKey.PREFERENCES,
|
||||||
|
value: { memories: { enabled: false } },
|
||||||
|
});
|
||||||
|
|
||||||
|
user = await this.findOrFail(user.id, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempPassword = user.shouldChangePassword ? rest.password : undefined;
|
||||||
|
if (notify) {
|
||||||
|
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id: user.id, tempPassword } });
|
||||||
|
}
|
||||||
|
return mapUserAdmin(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(auth: AuthDto, id: string): Promise<UserAdminResponseDto> {
|
||||||
|
const user = await this.findOrFail(id, { withDeleted: true });
|
||||||
|
return mapUserAdmin(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(auth: AuthDto, id: string, dto: UserAdminUpdateDto): Promise<UserAdminResponseDto> {
|
||||||
|
const user = await this.findOrFail(id, {});
|
||||||
|
|
||||||
|
if (dto.quotaSizeInBytes && user.quotaSizeInBytes !== dto.quotaSizeInBytes) {
|
||||||
|
await this.userRepository.syncUsage(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO replace with entire preferences object
|
||||||
|
if (dto.memoriesEnabled !== undefined || dto.avatarColor) {
|
||||||
|
const newPreferences = getPreferences(user);
|
||||||
|
if (dto.memoriesEnabled !== undefined) {
|
||||||
|
newPreferences.memories.enabled = dto.memoriesEnabled;
|
||||||
|
delete dto.memoriesEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.avatarColor) {
|
||||||
|
newPreferences.avatar.color = dto.avatarColor;
|
||||||
|
delete dto.avatarColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userRepository.upsertMetadata(id, {
|
||||||
|
key: UserMetadataKey.PREFERENCES,
|
||||||
|
value: getPreferencesPartial(user, newPreferences),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await this.userRepository.update(id, { ...dto, updatedAt: new Date() });
|
||||||
|
|
||||||
|
return mapUserAdmin(updatedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(auth: AuthDto, id: string, dto: UserAdminDeleteDto): Promise<UserAdminResponseDto> {
|
||||||
|
const { force } = dto;
|
||||||
|
const { isAdmin } = await this.findOrFail(id, {});
|
||||||
|
if (isAdmin) {
|
||||||
|
throw new ForbiddenException('Cannot delete admin user');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.albumRepository.softDeleteAll(id);
|
||||||
|
|
||||||
|
const status = force ? UserStatus.REMOVING : UserStatus.DELETED;
|
||||||
|
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapUserAdmin(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(auth: AuthDto, id: string): Promise<UserAdminResponseDto> {
|
||||||
|
await this.findOrFail(id, { withDeleted: true });
|
||||||
|
await this.albumRepository.restoreAll(id);
|
||||||
|
const user = await this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE });
|
||||||
|
return mapUserAdmin(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findOrFail(id: string, options: UserFindOptions) {
|
||||||
|
const user = await this.userRepository.get(id, options);
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('User not found');
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,5 @@
|
||||||
import {
|
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||||
BadRequestException,
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
ForbiddenException,
|
|
||||||
InternalServerErrorException,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { UpdateUserDto, mapUser } from 'src/dtos/user.dto';
|
|
||||||
import { UserEntity, UserStatus } from 'src/entities/user.entity';
|
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||||
|
@ -63,13 +57,13 @@ describe(UserService.name, () => {
|
||||||
describe('getAll', () => {
|
describe('getAll', () => {
|
||||||
it('should get all users', async () => {
|
it('should get all users', async () => {
|
||||||
userMock.getList.mockResolvedValue([userStub.admin]);
|
userMock.getList.mockResolvedValue([userStub.admin]);
|
||||||
await expect(sut.getAll(authStub.admin, false)).resolves.toEqual([
|
await expect(sut.search()).resolves.toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: authStub.admin.user.id,
|
id: authStub.admin.user.id,
|
||||||
email: authStub.admin.user.email,
|
email: authStub.admin.user.email,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: true });
|
expect(userMock.getList).toHaveBeenCalledWith({ withDeleted: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -82,255 +76,17 @@ describe(UserService.name, () => {
|
||||||
|
|
||||||
it('should throw an error if a user is not found', async () => {
|
it('should throw an error if a user is not found', async () => {
|
||||||
userMock.get.mockResolvedValue(null);
|
userMock.get.mockResolvedValue(null);
|
||||||
await expect(sut.get(authStub.admin.user.id)).rejects.toBeInstanceOf(NotFoundException);
|
await expect(sut.get(authStub.admin.user.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
|
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, { withDeleted: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getMe', () => {
|
describe('getMe', () => {
|
||||||
it("should get the auth user's info", async () => {
|
it("should get the auth user's info", () => {
|
||||||
userMock.get.mockResolvedValue(userStub.admin);
|
const user = authStub.admin.user;
|
||||||
await sut.getMe(authStub.admin);
|
expect(sut.getMe(authStub.admin)).toMatchObject({
|
||||||
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, {});
|
id: user.id,
|
||||||
});
|
email: user.email,
|
||||||
|
|
||||||
it('should throw an error if a user is not found', async () => {
|
|
||||||
userMock.get.mockResolvedValue(null);
|
|
||||||
await expect(sut.getMe(authStub.admin)).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
expect(userMock.get).toHaveBeenCalledWith(authStub.admin.user.id, {});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('update', () => {
|
|
||||||
it('should update user', async () => {
|
|
||||||
const update: UpdateUserDto = {
|
|
||||||
id: userStub.user1.id,
|
|
||||||
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({ user: { ...authStub.user1.user, isAdmin: true } }, 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, { id: userStub.user1.id, storageLabel: '' });
|
|
||||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
|
||||||
id: userStub.user1.id,
|
|
||||||
storageLabel: null,
|
|
||||||
updatedAt: expect.any(Date),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should omit a storage label set by non-admin users', async () => {
|
|
||||||
userMock.update.mockResolvedValue(userStub.user1);
|
|
||||||
await sut.update({ user: userStub.user1 }, { id: userStub.user1.id, storageLabel: 'admin' });
|
|
||||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
|
||||||
id: userStub.user1.id,
|
|
||||||
updatedAt: expect.any(Date),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('user can only update its information', async () => {
|
|
||||||
userMock.get.mockResolvedValueOnce({
|
|
||||||
...userStub.user1,
|
|
||||||
id: 'not_immich_auth_user_id',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = sut.update(
|
|
||||||
{ user: userStub.user1 },
|
|
||||||
{
|
|
||||||
id: 'not_immich_auth_user_id',
|
|
||||||
password: 'I take over your account now',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await expect(result).rejects.toBeInstanceOf(ForbiddenException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should let a user change their email', async () => {
|
|
||||||
const dto = { id: userStub.user1.id, email: 'updated@test.com' };
|
|
||||||
|
|
||||||
userMock.get.mockResolvedValue(userStub.user1);
|
|
||||||
userMock.update.mockResolvedValue(userStub.user1);
|
|
||||||
|
|
||||||
await sut.update({ user: userStub.user1 }, dto);
|
|
||||||
|
|
||||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
|
||||||
id: 'user-id',
|
|
||||||
email: 'updated@test.com',
|
|
||||||
updatedAt: expect.any(Date),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not let a user change their 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({ user: userStub.user1 }, 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, dto)).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
|
|
||||||
expect(userMock.update).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('admin can update any user information', async () => {
|
|
||||||
const update: UpdateUserDto = {
|
|
||||||
id: userStub.user1.id,
|
|
||||||
shouldChangePassword: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
userMock.update.mockResolvedValueOnce(userStub.user1);
|
|
||||||
await sut.update(authStub.admin, update);
|
|
||||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, {
|
|
||||||
id: 'user-id',
|
|
||||||
shouldChangePassword: true,
|
|
||||||
updatedAt: expect.any(Date),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('update user information should throw error if user not found', async () => {
|
|
||||||
userMock.get.mockResolvedValueOnce(null);
|
|
||||||
|
|
||||||
const result = sut.update(authStub.admin, {
|
|
||||||
id: userStub.user1.id,
|
|
||||||
shouldChangePassword: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(result).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should let the admin update himself', async () => {
|
|
||||||
const dto = { id: userStub.admin.id, shouldChangePassword: true, isAdmin: true };
|
|
||||||
|
|
||||||
userMock.update.mockResolvedValueOnce(userStub.admin);
|
|
||||||
|
|
||||||
await sut.update(authStub.admin, dto);
|
|
||||||
|
|
||||||
expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, { ...dto, updatedAt: expect.any(Date) });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not let the another user become an admin', async () => {
|
|
||||||
const dto = { id: userStub.user1.id, shouldChangePassword: true, isAdmin: true };
|
|
||||||
|
|
||||||
userMock.get.mockResolvedValueOnce(userStub.user1);
|
|
||||||
|
|
||||||
await expect(sut.update(authStub.admin, dto)).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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(mapUser(userStub.user1));
|
|
||||||
expect(userMock.update).toHaveBeenCalledWith(userStub.user1.id, { status: UserStatus.ACTIVE, deletedAt: null });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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(mapUser(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(
|
|
||||||
mapUser(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('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(mapUser(userStub.user1));
|
|
||||||
|
|
||||||
expect(userMock.getAdmin).toBeCalled();
|
|
||||||
expect(userMock.create).toBeCalledWith({
|
|
||||||
email: userStub.user1.email,
|
|
||||||
name: userStub.user1.name,
|
|
||||||
storageLabel: 'label',
|
|
||||||
password: expect.anything(),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { UserCore } from 'src/cores/user.core';
|
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
|
import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
|
||||||
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
import { UserMetadataKey } from 'src/entities/user-metadata.entity';
|
||||||
import { UserEntity, UserStatus } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
|
@ -21,73 +21,30 @@ import { getPreferences, getPreferencesPartial } from 'src/utils/preferences';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
private userCore: UserCore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@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(UserService.name);
|
this.logger.setContext(UserService.name);
|
||||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listUsers(): Promise<UserResponseDto[]> {
|
async search(): Promise<UserResponseDto[]> {
|
||||||
const users = await this.userRepository.getList({ withDeleted: true });
|
const users = await this.userRepository.getList({ withDeleted: false });
|
||||||
return users.map((user) => mapUser(user));
|
return users.map((user) => mapUser(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> {
|
getMe(auth: AuthDto): UserAdminResponseDto {
|
||||||
const users = await this.userRepository.getList({ withDeleted: !isAll });
|
return mapUserAdmin(auth.user);
|
||||||
return users.map((user) => mapUser(user));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(userId: string): Promise<UserResponseDto> {
|
async updateMe({ user }: AuthDto, dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
|
||||||
const user = await this.userRepository.get(userId, { withDeleted: false });
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException('User not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapUser(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMe(auth: AuthDto): Promise<UserResponseDto> {
|
|
||||||
return this.findOrFail(auth.user.id, {}).then(mapUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(dto: CreateUserDto): Promise<UserResponseDto> {
|
|
||||||
const { memoriesEnabled, notify, ...rest } = dto;
|
|
||||||
let user = await this.userCore.createUser(rest);
|
|
||||||
|
|
||||||
// TODO remove and replace with entire dto.preferences config
|
|
||||||
if (memoriesEnabled === false) {
|
|
||||||
await this.userRepository.upsertMetadata(user.id, {
|
|
||||||
key: UserMetadataKey.PREFERENCES,
|
|
||||||
value: { memories: { enabled: false } },
|
|
||||||
});
|
|
||||||
|
|
||||||
user = await this.findOrFail(user.id, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempPassword = user.shouldChangePassword ? rest.password : undefined;
|
|
||||||
if (notify) {
|
|
||||||
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id: user.id, tempPassword } });
|
|
||||||
}
|
|
||||||
return mapUser(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(auth: AuthDto, dto: UpdateUserDto): Promise<UserResponseDto> {
|
|
||||||
const user = await this.findOrFail(dto.id, {});
|
|
||||||
|
|
||||||
if (dto.quotaSizeInBytes && user.quotaSizeInBytes !== dto.quotaSizeInBytes) {
|
|
||||||
await this.userRepository.syncUsage(dto.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO replace with entire preferences object
|
// TODO replace with entire preferences object
|
||||||
if (dto.memoriesEnabled !== undefined || dto.avatarColor) {
|
if (dto.memoriesEnabled !== undefined || dto.avatarColor) {
|
||||||
const newPreferences = getPreferences(user);
|
const newPreferences = getPreferences(user);
|
||||||
|
@ -101,42 +58,40 @@ export class UserService {
|
||||||
delete dto.avatarColor;
|
delete dto.avatarColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userRepository.upsertMetadata(dto.id, {
|
await this.userRepository.upsertMetadata(user.id, {
|
||||||
key: UserMetadataKey.PREFERENCES,
|
key: UserMetadataKey.PREFERENCES,
|
||||||
value: getPreferencesPartial(user, newPreferences),
|
value: getPreferencesPartial(user, newPreferences),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await this.userCore.updateUser(auth.user, dto.id, dto);
|
if (dto.email) {
|
||||||
|
const duplicate = await this.userRepository.getByEmail(dto.email);
|
||||||
|
if (duplicate && duplicate.id !== user.id) {
|
||||||
|
throw new BadRequestException('Email already in use by another account');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return mapUser(updatedUser);
|
const update: Partial<UserEntity> = {
|
||||||
|
email: dto.email,
|
||||||
|
name: dto.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dto.password) {
|
||||||
|
const hashedPassword = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
|
||||||
|
update.password = hashedPassword;
|
||||||
|
update.shouldChangePassword = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await this.userRepository.update(user.id, update);
|
||||||
|
|
||||||
|
return mapUserAdmin(updatedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(auth: AuthDto, id: string, dto: DeleteUserDto): Promise<UserResponseDto> {
|
async get(id: string): Promise<UserResponseDto> {
|
||||||
const { force } = dto;
|
const user = await this.findOrFail(id, { withDeleted: false });
|
||||||
const { isAdmin } = await this.findOrFail(id, {});
|
|
||||||
if (isAdmin) {
|
|
||||||
throw new ForbiddenException('Cannot delete admin user');
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.albumRepository.softDeleteAll(id);
|
|
||||||
|
|
||||||
const status = force ? UserStatus.REMOVING : UserStatus.DELETED;
|
|
||||||
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
|
||||||
|
|
||||||
if (force) {
|
|
||||||
await this.jobRepository.queue({ name: JobName.USER_DELETION, data: { id: user.id, force } });
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapUser(user);
|
return mapUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(auth: AuthDto, id: string): Promise<UserResponseDto> {
|
|
||||||
await this.findOrFail(id, { withDeleted: true });
|
|
||||||
await this.albumRepository.restoreAll(id);
|
|
||||||
return this.userRepository.update(id, { deletedAt: null, status: UserStatus.ACTIVE }).then(mapUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
|
async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise<CreateProfileImageResponseDto> {
|
||||||
const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false });
|
const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false });
|
||||||
const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path });
|
const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path });
|
||||||
|
|
|
@ -154,7 +154,7 @@ export function validateCronExpression(expression: string) {
|
||||||
|
|
||||||
type IValue = { value: string };
|
type IValue = { value: string };
|
||||||
|
|
||||||
export const toEmail = ({ value }: IValue) => value?.toLowerCase();
|
export const toEmail = ({ value }: IValue) => (value ? value.toLowerCase() : value);
|
||||||
|
|
||||||
export const toSanitized = ({ value }: IValue) => sanitize((value || '').replaceAll('.', ''));
|
export const toSanitized = ({ value }: IValue) => sanitize((value || '').replaceAll('.', ''));
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { deleteUser, type UserResponseDto } from '@immich/sdk';
|
import { deleteUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||||
import { serverConfig } from '$lib/stores/server-config.store';
|
import { serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||||
|
@ -20,9 +20,9 @@
|
||||||
|
|
||||||
const handleDeleteUser = async () => {
|
const handleDeleteUser = async () => {
|
||||||
try {
|
try {
|
||||||
const { deletedAt } = await deleteUser({
|
const { deletedAt } = await deleteUserAdmin({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
deleteUserDto: { force: forceDelete },
|
userAdminDeleteDto: { force: forceDelete },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletedAt == undefined) {
|
if (deletedAt == undefined) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { restoreUser, type UserResponseDto } from '@immich/sdk';
|
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
const handleRestoreUser = async () => {
|
const handleRestoreUser = async () => {
|
||||||
try {
|
try {
|
||||||
const { deletedAt } = await restoreUser({ id: user.id });
|
const { deletedAt } = await restoreUserAdmin({ id: user.id });
|
||||||
if (deletedAt == undefined) {
|
if (deletedAt == undefined) {
|
||||||
dispatch('success');
|
dispatch('success');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
getMyUserInfo,
|
getMyUser,
|
||||||
removeUserFromAlbum,
|
removeUserFromAlbum,
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type UserResponseDto,
|
type UserResponseDto,
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
currentUser = await getMyUserInfo();
|
currentUser = await getMyUser();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to refresh user');
|
handleError(error, 'Unable to refresh user');
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import {
|
import {
|
||||||
AlbumUserRole,
|
AlbumUserRole,
|
||||||
getAllSharedLinks,
|
getAllSharedLinks,
|
||||||
getAllUsers,
|
searchUsers,
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AlbumUserAddDto,
|
type AlbumUserAddDto,
|
||||||
type SharedLinkResponseDto,
|
type SharedLinkResponseDto,
|
||||||
|
@ -36,10 +36,10 @@
|
||||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await getSharedLinks();
|
await getSharedLinks();
|
||||||
const data = await getAllUsers({ isAll: false });
|
const data = await searchUsers();
|
||||||
|
|
||||||
// remove invalid users
|
// remove album owner
|
||||||
users = data.filter((user) => !(user.deletedAt || user.id === album.ownerId));
|
users = data.filter((user) => user.id !== album.ownerId);
|
||||||
|
|
||||||
// Remove the existed shared users from the album
|
// Remove the existed shared users from the album
|
||||||
for (const sharedUser of album.albumUsers) {
|
for (const sharedUser of album.albumUsers) {
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import PasswordField from '../shared-components/password-field.svelte';
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
import { updateMyUser } from '@immich/sdk';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
|
||||||
let errorMessage: string;
|
let errorMessage: string;
|
||||||
let success: string;
|
let success: string;
|
||||||
|
|
||||||
|
@ -31,13 +30,7 @@
|
||||||
if (valid) {
|
if (valid) {
|
||||||
errorMessage = '';
|
errorMessage = '';
|
||||||
|
|
||||||
await updateUser({
|
await updateMyUser({ userUpdateMeDto: { password: String(password) } });
|
||||||
updateUserDto: {
|
|
||||||
id: user.id,
|
|
||||||
password: String(password),
|
|
||||||
shouldChangePassword: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch('success');
|
dispatch('success');
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { serverInfo } from '$lib/stores/server-info.store';
|
import { serverInfo } from '$lib/stores/server-info.store';
|
||||||
import { convertToBytes } from '$lib/utils/byte-converter';
|
import { convertToBytes } from '$lib/utils/byte-converter';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { createUser } from '@immich/sdk';
|
import { createUserAdmin } from '@immich/sdk';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import PasswordField from '../shared-components/password-field.svelte';
|
import PasswordField from '../shared-components/password-field.svelte';
|
||||||
|
@ -49,8 +49,8 @@
|
||||||
error = '';
|
error = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createUser({
|
await createUserAdmin({
|
||||||
createUserDto: {
|
userAdminCreateDto: {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
shouldChangePassword,
|
shouldChangePassword,
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||||
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { serverInfo } from '$lib/stores/server-info.store';
|
import { serverInfo } from '$lib/stores/server-info.store';
|
||||||
import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
|
import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||||
|
import { mdiAccountEditOutline } from '@mdi/js';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
|
||||||
import { mdiAccountEditOutline } from '@mdi/js';
|
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserAdminResponseDto;
|
||||||
export let canResetPassword = true;
|
export let canResetPassword = true;
|
||||||
export let newPassword: string;
|
export let newPassword: string;
|
||||||
export let onClose: () => void;
|
export let onClose: () => void;
|
||||||
|
@ -36,9 +36,9 @@
|
||||||
const editUser = async () => {
|
const editUser = async () => {
|
||||||
try {
|
try {
|
||||||
const { id, email, name, storageLabel } = user;
|
const { id, email, name, storageLabel } = user;
|
||||||
await updateUser({
|
await updateUserAdmin({
|
||||||
updateUserDto: {
|
id,
|
||||||
id,
|
userAdminUpdateDto: {
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
storageLabel: storageLabel || '',
|
storageLabel: storageLabel || '',
|
||||||
|
@ -56,9 +56,9 @@
|
||||||
try {
|
try {
|
||||||
newPassword = generatePassword();
|
newPassword = generatePassword();
|
||||||
|
|
||||||
await updateUser({
|
await updateUserAdmin({
|
||||||
updateUserDto: {
|
id: user.id,
|
||||||
id: user.id,
|
userAdminUpdateDto: {
|
||||||
password: newPassword,
|
password: newPassword,
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||||
import { mdiFolderSync } from '@mdi/js';
|
import { mdiFolderSync } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { getAllUsers } from '@immich/sdk';
|
import { searchUsersAdmin } from '@immich/sdk';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
let userOptions: { value: string; text: string }[] = [];
|
let userOptions: { value: string; text: string }[] = [];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const users = await getAllUsers({ isAll: true });
|
const users = await searchUsersAdmin({});
|
||||||
userOptions = users.map((user) => ({ value: user.id, text: user.name }));
|
userOptions = users.map((user) => ({ value: user.id, text: user.name }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { deleteProfileImage, updateUser, type UserAvatarColor } from '@immich/sdk';
|
import { deleteProfileImage, updateMyUser, type UserAvatarColor } from '@immich/sdk';
|
||||||
import { mdiCog, mdiLogout, mdiPencil } from '@mdi/js';
|
import { mdiCog, mdiLogout, mdiPencil } from '@mdi/js';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
@ -27,9 +27,8 @@
|
||||||
await deleteProfileImage();
|
await deleteProfileImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = await updateUser({
|
$user = await updateMyUser({
|
||||||
updateUserDto: {
|
userUpdateMeDto: {
|
||||||
id: $user.id,
|
|
||||||
email: $user.email,
|
email: $user.email,
|
||||||
name: $user.name,
|
name: $user.name,
|
||||||
avatarColor: color,
|
avatarColor: color,
|
||||||
|
|
|
@ -3,23 +3,18 @@
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
import { updateMyUser, type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
|
|
||||||
import Button from '../elements/buttons/button.svelte';
|
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserAdminResponseDto;
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await updateUser({
|
const data = await updateMyUser({ userUpdateMeDto: { memoriesEnabled: user.memoriesEnabled } });
|
||||||
updateUserDto: {
|
|
||||||
id: user.id,
|
|
||||||
memoriesEnabled: user.memoriesEnabled,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(user, data);
|
Object.assign(user, data);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { oauth } from '$lib/utils';
|
import { oauth } from '$lib/utils';
|
||||||
import { type UserResponseDto } from '@immich/sdk';
|
import { type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { handleError } from '../../utils/handle-error';
|
import { handleError } from '../../utils/handle-error';
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserAdminResponseDto;
|
||||||
|
|
||||||
let loading = true;
|
let loading = true;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getAllUsers, getPartners, type UserResponseDto } from '@immich/sdk';
|
import { searchUsers, getPartners, type UserResponseDto } from '@immich/sdk';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||||
|
@ -14,11 +14,10 @@
|
||||||
const dispatch = createEventDispatcher<{ 'add-users': UserResponseDto[] }>();
|
const dispatch = createEventDispatcher<{ 'add-users': UserResponseDto[] }>();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// TODO: update endpoint to have a query param for deleted users
|
let users = await searchUsers();
|
||||||
let users = await getAllUsers({ isAll: false });
|
|
||||||
|
|
||||||
// remove invalid users
|
// remove current user
|
||||||
users = users.filter((_user) => !(_user.deletedAt || _user.id === user.id));
|
users = users.filter((_user) => _user.id !== user.id);
|
||||||
|
|
||||||
// exclude partners from the list of users available for selection
|
// exclude partners from the list of users available for selection
|
||||||
const partners = await getPartners({ direction: 'shared-by' });
|
const partners = await getPartners({ direction: 'shared-by' });
|
||||||
|
|
|
@ -3,23 +3,22 @@
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import { handleError } from '../../utils/handle-error';
|
|
||||||
import Button from '../elements/buttons/button.svelte';
|
|
||||||
import { user } from '$lib/stores/user.store';
|
|
||||||
import { cloneDeep } from 'lodash-es';
|
|
||||||
import { updateUser } from '@immich/sdk';
|
|
||||||
import SettingInputField, {
|
import SettingInputField, {
|
||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
|
import { user } from '$lib/stores/user.store';
|
||||||
|
import { updateMyUser } from '@immich/sdk';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { handleError } from '../../utils/handle-error';
|
||||||
|
import Button from '../elements/buttons/button.svelte';
|
||||||
|
|
||||||
let editedUser = cloneDeep($user);
|
let editedUser = cloneDeep($user);
|
||||||
|
|
||||||
const handleSaveProfile = async () => {
|
const handleSaveProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await updateUser({
|
const data = await updateMyUser({
|
||||||
updateUserDto: {
|
userUpdateMeDto: {
|
||||||
id: editedUser.id,
|
|
||||||
email: editedUser.email,
|
email: editedUser.email,
|
||||||
name: editedUser.name,
|
name: editedUser.name,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type { UserResponseDto } from '@immich/sdk';
|
import type { UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const user = writable<UserResponseDto>();
|
export const user = writable<UserAdminResponseDto>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the store to its initial undefined value. Make sure to
|
* Reset the store to its initial undefined value. Make sure to
|
||||||
* only do this _after_ redirecting to an unauthenticated page.
|
* only do this _after_ redirecting to an unauthenticated page.
|
||||||
*/
|
*/
|
||||||
export const resetSavedUser = () => {
|
export const resetSavedUser = () => {
|
||||||
user.set(undefined as unknown as UserResponseDto);
|
user.set(undefined as unknown as UserAdminResponseDto);
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
startOAuth,
|
startOAuth,
|
||||||
unlinkOAuthAccount,
|
unlinkOAuthAccount,
|
||||||
type SharedLinkResponseDto,
|
type SharedLinkResponseDto,
|
||||||
type UserResponseDto,
|
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js';
|
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js';
|
||||||
|
|
||||||
|
@ -264,7 +263,7 @@ export const oauth = {
|
||||||
login: (location: Location) => {
|
login: (location: Location) => {
|
||||||
return finishOAuth({ oAuthCallbackDto: { url: location.href } });
|
return finishOAuth({ oAuthCallbackDto: { url: location.href } });
|
||||||
},
|
},
|
||||||
link: (location: Location): Promise<UserResponseDto> => {
|
link: (location: Location) => {
|
||||||
return linkOAuthAccount({ oAuthCallbackDto: { url: location.href } });
|
return linkOAuthAccount({ oAuthCallbackDto: { url: location.href } });
|
||||||
},
|
},
|
||||||
unlink: () => {
|
unlink: () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { serverInfo } from '$lib/stores/server-info.store';
|
import { serverInfo } from '$lib/stores/server-info.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { getMyUserInfo, getStorage } from '@immich/sdk';
|
import { getMyUser, getStorage } from '@immich/sdk';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { AppRoute } from '../constants';
|
import { AppRoute } from '../constants';
|
||||||
|
@ -15,7 +15,7 @@ export const loadUser = async () => {
|
||||||
try {
|
try {
|
||||||
let loaded = get(user);
|
let loaded = get(user);
|
||||||
if (!loaded && hasAuthCookie()) {
|
if (!loaded && hasAuthCookie()) {
|
||||||
loaded = await getMyUserInfo();
|
loaded = await getMyUser();
|
||||||
user.set(loaded);
|
user.set(loaded);
|
||||||
}
|
}
|
||||||
return loaded;
|
return loaded;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
||||||
import { getUserById } from '@immich/sdk';
|
import { getUser } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
export const load = (async ({ params }) => {
|
||||||
await authenticate();
|
await authenticate();
|
||||||
|
|
||||||
const partner = await getUserById({ id: params.userId });
|
const partner = await getUser({ id: params.userId });
|
||||||
const asset = await getAssetInfoFromParam(params);
|
const asset = await getAssetInfoFromParam(params);
|
||||||
return {
|
return {
|
||||||
asset,
|
asset,
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
deleteLibrary,
|
deleteLibrary,
|
||||||
getAllLibraries,
|
getAllLibraries,
|
||||||
getLibraryStatistics,
|
getLibraryStatistics,
|
||||||
getUserById,
|
getUserAdmin,
|
||||||
removeOfflineFiles,
|
removeOfflineFiles,
|
||||||
scanLibrary,
|
scanLibrary,
|
||||||
updateLibrary,
|
updateLibrary,
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
|
|
||||||
const refreshStats = async (listIndex: number) => {
|
const refreshStats = async (listIndex: number) => {
|
||||||
stats[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id });
|
stats[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id });
|
||||||
owner[listIndex] = await getUserById({ id: libraries[listIndex].ownerId });
|
owner[listIndex] = await getUserAdmin({ id: libraries[listIndex].ownerId });
|
||||||
photos[listIndex] = stats[listIndex].photos;
|
photos[listIndex] = stats[listIndex].photos;
|
||||||
videos[listIndex] = stats[listIndex].videos;
|
videos[listIndex] = stats[listIndex].videos;
|
||||||
totalCount[listIndex] = stats[listIndex].total;
|
totalCount[listIndex] = stats[listIndex].total;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||||
import { getAllUsers } from '@immich/sdk';
|
import { searchUsersAdmin } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
await authenticate({ admin: true });
|
await authenticate({ admin: true });
|
||||||
await requestServerInfo();
|
await requestServerInfo();
|
||||||
const allUsers = await getAllUsers({ isAll: false });
|
const allUsers = await searchUsersAdmin({ withDeleted: false });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allUsers,
|
allUsers,
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
|
||||||
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
|
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
|
||||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
|
||||||
import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
|
import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
|
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||||
import {
|
import {
|
||||||
NotificationType,
|
NotificationType,
|
||||||
notificationController,
|
notificationController,
|
||||||
|
@ -17,28 +18,27 @@
|
||||||
import { serverConfig } from '$lib/stores/server-config.store';
|
import { serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { asByteUnitString } from '$lib/utils/byte-units';
|
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
|
import { asByteUnitString } from '$lib/utils/byte-units';
|
||||||
|
import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import { mdiClose, mdiContentCopy, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
import { mdiClose, mdiContentCopy, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
let allUsers: UserResponseDto[] = [];
|
let allUsers: UserAdminResponseDto[] = [];
|
||||||
let shouldShowEditUserForm = false;
|
let shouldShowEditUserForm = false;
|
||||||
let shouldShowCreateUserForm = false;
|
let shouldShowCreateUserForm = false;
|
||||||
let shouldShowPasswordResetSuccess = false;
|
let shouldShowPasswordResetSuccess = false;
|
||||||
let shouldShowDeleteConfirmDialog = false;
|
let shouldShowDeleteConfirmDialog = false;
|
||||||
let shouldShowRestoreDialog = false;
|
let shouldShowRestoreDialog = false;
|
||||||
let selectedUser: UserResponseDto;
|
let selectedUser: UserAdminResponseDto;
|
||||||
let newPassword: string;
|
let newPassword: string;
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
allUsers = await getAllUsers({ isAll: false });
|
allUsers = await searchUsersAdmin({ withDeleted: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDeleteSuccess = (userId: string) => {
|
const onDeleteSuccess = (userId: string) => {
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
shouldShowCreateUserForm = false;
|
shouldShowCreateUserForm = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const editUserHandler = (user: UserResponseDto) => {
|
const editUserHandler = (user: UserAdminResponseDto) => {
|
||||||
selectedUser = user;
|
selectedUser = user;
|
||||||
shouldShowEditUserForm = true;
|
shouldShowEditUserForm = true;
|
||||||
};
|
};
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
shouldShowPasswordResetSuccess = true;
|
shouldShowPasswordResetSuccess = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteUserHandler = (user: UserResponseDto) => {
|
const deleteUserHandler = (user: UserAdminResponseDto) => {
|
||||||
selectedUser = user;
|
selectedUser = user;
|
||||||
shouldShowDeleteConfirmDialog = true;
|
shouldShowDeleteConfirmDialog = true;
|
||||||
};
|
};
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
shouldShowDeleteConfirmDialog = false;
|
shouldShowDeleteConfirmDialog = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreUserHandler = (user: UserResponseDto) => {
|
const restoreUserHandler = (user: UserAdminResponseDto) => {
|
||||||
selectedUser = user;
|
selectedUser = user;
|
||||||
shouldShowRestoreDialog = true;
|
shouldShowRestoreDialog = true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||||
import { getAllUsers } from '@immich/sdk';
|
import { searchUsersAdmin } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
await authenticate({ admin: true });
|
await authenticate({ admin: true });
|
||||||
await requestServerInfo();
|
await requestServerInfo();
|
||||||
const allUsers = await getAllUsers({ isAll: false });
|
const allUsers = await searchUsersAdmin({ withDeleted: true });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allUsers,
|
allUsers,
|
||||||
|
|
|
@ -25,5 +25,5 @@
|
||||||
enter the new password below.
|
enter the new password below.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ChangePasswordForm user={$user} on:success={onSuccess} />
|
<ChangePasswordForm on:success={onSuccess} />
|
||||||
</FullscreenContainer>
|
</FullscreenContainer>
|
||||||
|
|
|
@ -1,22 +1,11 @@
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { UserAvatarColor, UserStatus, type UserResponseDto } from '@immich/sdk';
|
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
|
||||||
import { Sync } from 'factory.ts';
|
import { Sync } from 'factory.ts';
|
||||||
|
|
||||||
export const userFactory = Sync.makeFactory<UserResponseDto>({
|
export const userFactory = Sync.makeFactory<UserResponseDto>({
|
||||||
id: Sync.each(() => faker.string.uuid()),
|
id: Sync.each(() => faker.string.uuid()),
|
||||||
email: Sync.each(() => faker.internet.email()),
|
email: Sync.each(() => faker.internet.email()),
|
||||||
name: Sync.each(() => faker.person.fullName()),
|
name: Sync.each(() => faker.person.fullName()),
|
||||||
storageLabel: Sync.each(() => faker.string.alphanumeric()),
|
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
shouldChangePassword: Sync.each(() => faker.datatype.boolean()),
|
|
||||||
isAdmin: true,
|
|
||||||
createdAt: Sync.each(() => faker.date.past().toISOString()),
|
|
||||||
deletedAt: null,
|
|
||||||
updatedAt: Sync.each(() => faker.date.past().toISOString()),
|
|
||||||
memoriesEnabled: true,
|
|
||||||
oauthId: '',
|
|
||||||
avatarColor: UserAvatarColor.Primary,
|
avatarColor: UserAvatarColor.Primary,
|
||||||
quotaUsageInBytes: 0,
|
|
||||||
quotaSizeInBytes: null,
|
|
||||||
status: UserStatus.Active,
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue