mirror of
https://github.com/immich-app/immich.git
synced 2025-01-27 22:22:45 +01:00
parent
78c7ff855d
commit
171b6bb0a6
32 changed files with 362 additions and 73 deletions
e2e/src
mobile/openapi
open-api
server/src
controllers
dtos
repositories
services
web/src/routes/auth/onboarding
|
@ -1,4 +1,4 @@
|
||||||
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { app, utils } from 'src/utils';
|
||||||
|
@ -162,19 +162,4 @@ describe('/server-info', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /server-info/admin-onboarding', () => {
|
|
||||||
it('should set admin onboarding', async () => {
|
|
||||||
const config = await getServerConfig({});
|
|
||||||
expect(config.isOnboarded).toBe(false);
|
|
||||||
|
|
||||||
const { status } = await request(app)
|
|
||||||
.post('/server-info/admin-onboarding')
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
const newConfig = await getServerConfig({});
|
|
||||||
expect(newConfig.isOnboarded).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
76
e2e/src/api/specs/system-metadata.e2e-spec.ts
Normal file
76
e2e/src/api/specs/system-metadata.e2e-spec.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
||||||
|
import { createUserDto } from 'src/fixtures';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { app, utils } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('/server-info', () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
let nonAdmin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup({ onboarding: false });
|
||||||
|
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /system-metadata/admin-onboarding', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only work for admins', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/system-metadata/admin-onboarding')
|
||||||
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
|
||||||
|
.send({ isOnboarded: true });
|
||||||
|
expect(status).toBe(403);
|
||||||
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set admin onboarding', async () => {
|
||||||
|
const config = await getServerConfig({});
|
||||||
|
expect(config.isOnboarded).toBe(false);
|
||||||
|
|
||||||
|
const { status } = await request(app)
|
||||||
|
.post('/system-metadata/admin-onboarding')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ isOnboarded: true });
|
||||||
|
expect(status).toBe(204);
|
||||||
|
|
||||||
|
const newConfig = await getServerConfig({});
|
||||||
|
expect(newConfig.isOnboarded).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /system-metadata/reverse-geocoding-state', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only work for admins', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/system-metadata/reverse-geocoding-state')
|
||||||
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||||
|
expect(status).toBe(403);
|
||||||
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get the reverse geocoding state', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/system-metadata/reverse-geocoding-state')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
lastUpdate: expect.any(String),
|
||||||
|
lastImportFileName: 'cities500.txt',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -24,8 +24,8 @@ import {
|
||||||
getConfigDefaults,
|
getConfigDefaults,
|
||||||
login,
|
login,
|
||||||
searchMetadata,
|
searchMetadata,
|
||||||
setAdminOnboarding,
|
|
||||||
signUpAdmin,
|
signUpAdmin,
|
||||||
|
updateAdminOnboarding,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
validate,
|
validate,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
@ -264,7 +264,10 @@ export const utils = {
|
||||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||||
const response = await login({ loginCredentialDto: loginDto.admin });
|
const response = await login({ loginCredentialDto: loginDto.admin });
|
||||||
if (options.onboarding) {
|
if (options.onboarding) {
|
||||||
await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) });
|
await updateAdminOnboarding(
|
||||||
|
{ adminOnboardingUpdateDto: { isOnboarded: true } },
|
||||||
|
{ headers: asBearerAuth(response.accessToken) },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
9
mobile/openapi/.openapi-generator/FILES
generated
9
mobile/openapi/.openapi-generator/FILES
generated
|
@ -13,6 +13,7 @@ doc/ActivityCreateDto.md
|
||||||
doc/ActivityResponseDto.md
|
doc/ActivityResponseDto.md
|
||||||
doc/ActivityStatisticsResponseDto.md
|
doc/ActivityStatisticsResponseDto.md
|
||||||
doc/AddUsersDto.md
|
doc/AddUsersDto.md
|
||||||
|
doc/AdminOnboardingUpdateDto.md
|
||||||
doc/AlbumApi.md
|
doc/AlbumApi.md
|
||||||
doc/AlbumCountResponseDto.md
|
doc/AlbumCountResponseDto.md
|
||||||
doc/AlbumResponseDto.md
|
doc/AlbumResponseDto.md
|
||||||
|
@ -123,6 +124,7 @@ doc/QueueStatusDto.md
|
||||||
doc/ReactionLevel.md
|
doc/ReactionLevel.md
|
||||||
doc/ReactionType.md
|
doc/ReactionType.md
|
||||||
doc/RecognitionConfig.md
|
doc/RecognitionConfig.md
|
||||||
|
doc/ReverseGeocodingStateResponseDto.md
|
||||||
doc/ScanLibraryDto.md
|
doc/ScanLibraryDto.md
|
||||||
doc/SearchAlbumResponseDto.md
|
doc/SearchAlbumResponseDto.md
|
||||||
doc/SearchApi.md
|
doc/SearchApi.md
|
||||||
|
@ -174,6 +176,7 @@ doc/SystemConfigTemplateStorageOptionDto.md
|
||||||
doc/SystemConfigThemeDto.md
|
doc/SystemConfigThemeDto.md
|
||||||
doc/SystemConfigTrashDto.md
|
doc/SystemConfigTrashDto.md
|
||||||
doc/SystemConfigUserDto.md
|
doc/SystemConfigUserDto.md
|
||||||
|
doc/SystemMetadataApi.md
|
||||||
doc/TagApi.md
|
doc/TagApi.md
|
||||||
doc/TagResponseDto.md
|
doc/TagResponseDto.md
|
||||||
doc/TagTypeEnum.md
|
doc/TagTypeEnum.md
|
||||||
|
@ -226,6 +229,7 @@ lib/api/sessions_api.dart
|
||||||
lib/api/shared_link_api.dart
|
lib/api/shared_link_api.dart
|
||||||
lib/api/sync_api.dart
|
lib/api/sync_api.dart
|
||||||
lib/api/system_config_api.dart
|
lib/api/system_config_api.dart
|
||||||
|
lib/api/system_metadata_api.dart
|
||||||
lib/api/tag_api.dart
|
lib/api/tag_api.dart
|
||||||
lib/api/timeline_api.dart
|
lib/api/timeline_api.dart
|
||||||
lib/api/trash_api.dart
|
lib/api/trash_api.dart
|
||||||
|
@ -242,6 +246,7 @@ lib/model/activity_create_dto.dart
|
||||||
lib/model/activity_response_dto.dart
|
lib/model/activity_response_dto.dart
|
||||||
lib/model/activity_statistics_response_dto.dart
|
lib/model/activity_statistics_response_dto.dart
|
||||||
lib/model/add_users_dto.dart
|
lib/model/add_users_dto.dart
|
||||||
|
lib/model/admin_onboarding_update_dto.dart
|
||||||
lib/model/album_count_response_dto.dart
|
lib/model/album_count_response_dto.dart
|
||||||
lib/model/album_response_dto.dart
|
lib/model/album_response_dto.dart
|
||||||
lib/model/all_job_status_response_dto.dart
|
lib/model/all_job_status_response_dto.dart
|
||||||
|
@ -343,6 +348,7 @@ lib/model/queue_status_dto.dart
|
||||||
lib/model/reaction_level.dart
|
lib/model/reaction_level.dart
|
||||||
lib/model/reaction_type.dart
|
lib/model/reaction_type.dart
|
||||||
lib/model/recognition_config.dart
|
lib/model/recognition_config.dart
|
||||||
|
lib/model/reverse_geocoding_state_response_dto.dart
|
||||||
lib/model/scan_library_dto.dart
|
lib/model/scan_library_dto.dart
|
||||||
lib/model/search_album_response_dto.dart
|
lib/model/search_album_response_dto.dart
|
||||||
lib/model/search_asset_response_dto.dart
|
lib/model/search_asset_response_dto.dart
|
||||||
|
@ -419,6 +425,7 @@ test/activity_create_dto_test.dart
|
||||||
test/activity_response_dto_test.dart
|
test/activity_response_dto_test.dart
|
||||||
test/activity_statistics_response_dto_test.dart
|
test/activity_statistics_response_dto_test.dart
|
||||||
test/add_users_dto_test.dart
|
test/add_users_dto_test.dart
|
||||||
|
test/admin_onboarding_update_dto_test.dart
|
||||||
test/album_api_test.dart
|
test/album_api_test.dart
|
||||||
test/album_count_response_dto_test.dart
|
test/album_count_response_dto_test.dart
|
||||||
test/album_response_dto_test.dart
|
test/album_response_dto_test.dart
|
||||||
|
@ -534,6 +541,7 @@ test/queue_status_dto_test.dart
|
||||||
test/reaction_level_test.dart
|
test/reaction_level_test.dart
|
||||||
test/reaction_type_test.dart
|
test/reaction_type_test.dart
|
||||||
test/recognition_config_test.dart
|
test/recognition_config_test.dart
|
||||||
|
test/reverse_geocoding_state_response_dto_test.dart
|
||||||
test/scan_library_dto_test.dart
|
test/scan_library_dto_test.dart
|
||||||
test/search_album_response_dto_test.dart
|
test/search_album_response_dto_test.dart
|
||||||
test/search_api_test.dart
|
test/search_api_test.dart
|
||||||
|
@ -585,6 +593,7 @@ test/system_config_template_storage_option_dto_test.dart
|
||||||
test/system_config_theme_dto_test.dart
|
test/system_config_theme_dto_test.dart
|
||||||
test/system_config_trash_dto_test.dart
|
test/system_config_trash_dto_test.dart
|
||||||
test/system_config_user_dto_test.dart
|
test/system_config_user_dto_test.dart
|
||||||
|
test/system_metadata_api_test.dart
|
||||||
test/tag_api_test.dart
|
test/tag_api_test.dart
|
||||||
test/tag_response_dto_test.dart
|
test/tag_response_dto_test.dart
|
||||||
test/tag_type_enum_test.dart
|
test/tag_type_enum_test.dart
|
||||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AdminOnboardingUpdateDto.md
generated
Normal file
BIN
mobile/openapi/doc/AdminOnboardingUpdateDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/ReverseGeocodingStateResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/ReverseGeocodingStateResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/ServerInfoApi.md
generated
BIN
mobile/openapi/doc/ServerInfoApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemMetadataApi.md
generated
Normal file
BIN
mobile/openapi/doc/SystemMetadataApi.md
generated
Normal file
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/server_info_api.dart
generated
BIN
mobile/openapi/lib/api/server_info_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/system_metadata_api.dart
generated
Normal file
BIN
mobile/openapi/lib/api/system_metadata_api.dart
generated
Normal file
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/admin_onboarding_update_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/admin_onboarding_update_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/admin_onboarding_update_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/admin_onboarding_update_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/reverse_geocoding_state_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/server_info_api_test.dart
generated
BIN
mobile/openapi/test/server_info_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_metadata_api_test.dart
generated
Normal file
BIN
mobile/openapi/test/system_metadata_api_test.dart
generated
Normal file
Binary file not shown.
|
@ -4908,31 +4908,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/server-info/admin-onboarding": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "setAdminOnboarding",
|
|
||||||
"parameters": [],
|
|
||||||
"responses": {
|
|
||||||
"204": {
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Server Info"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/server-info/config": {
|
"/server-info/config": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getServerConfig",
|
"operationId": "getServerConfig",
|
||||||
|
@ -5885,6 +5860,103 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/system-metadata/admin-onboarding": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getAdminOnboarding",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AdminOnboardingUpdateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"System Metadata"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "updateAdminOnboarding",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AdminOnboardingUpdateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"System Metadata"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/system-metadata/reverse-geocoding-state": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getReverseGeocodingState",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ReverseGeocodingStateResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"System Metadata"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/tag": {
|
"/tag": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAllTags",
|
"operationId": "getAllTags",
|
||||||
|
@ -7180,6 +7252,17 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"AdminOnboardingUpdateDto": {
|
||||||
|
"properties": {
|
||||||
|
"isOnboarded": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isOnboarded"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"AlbumCountResponseDto": {
|
"AlbumCountResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"notShared": {
|
"notShared": {
|
||||||
|
@ -9618,6 +9701,23 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"ReverseGeocodingStateResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"lastImportFileName": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"lastUpdate": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"lastImportFileName",
|
||||||
|
"lastUpdate"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ScanLibraryDto": {
|
"ScanLibraryDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"refreshAllFiles": {
|
"refreshAllFiles": {
|
||||||
|
|
|
@ -998,6 +998,13 @@ export type SystemConfigTemplateStorageOptionDto = {
|
||||||
weekOptions: string[];
|
weekOptions: string[];
|
||||||
yearOptions: string[];
|
yearOptions: string[];
|
||||||
};
|
};
|
||||||
|
export type AdminOnboardingUpdateDto = {
|
||||||
|
isOnboarded: boolean;
|
||||||
|
};
|
||||||
|
export type ReverseGeocodingStateResponseDto = {
|
||||||
|
lastImportFileName: string | null;
|
||||||
|
lastUpdate: string | null;
|
||||||
|
};
|
||||||
export type CreateTagDto = {
|
export type CreateTagDto = {
|
||||||
name: string;
|
name: string;
|
||||||
"type": TagTypeEnum;
|
"type": TagTypeEnum;
|
||||||
|
@ -2330,12 +2337,6 @@ export function getServerInfo(opts?: Oazapfts.RequestOpts) {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function setAdminOnboarding(opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchText("/server-info/admin-onboarding", {
|
|
||||||
...opts,
|
|
||||||
method: "POST"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function getServerConfig(opts?: Oazapfts.RequestOpts) {
|
export function getServerConfig(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
@ -2597,6 +2598,31 @@ export function getStorageTemplateOptions(opts?: Oazapfts.RequestOpts) {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function getAdminOnboarding(opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: AdminOnboardingUpdateDto;
|
||||||
|
}>("/system-metadata/admin-onboarding", {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function updateAdminOnboarding({ adminOnboardingUpdateDto }: {
|
||||||
|
adminOnboardingUpdateDto: AdminOnboardingUpdateDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/system-metadata/admin-onboarding", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: adminOnboardingUpdateDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function getReverseGeocodingState(opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: ReverseGeocodingStateResponseDto;
|
||||||
|
}>("/system-metadata/reverse-geocoding-state", {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function getAllTags(opts?: Oazapfts.RequestOpts) {
|
export function getAllTags(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { SessionController } from 'src/controllers/session.controller';
|
||||||
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
||||||
import { SyncController } from 'src/controllers/sync.controller';
|
import { SyncController } from 'src/controllers/sync.controller';
|
||||||
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
||||||
|
import { SystemMetadataController } from 'src/controllers/system-metadata.controller';
|
||||||
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';
|
||||||
|
@ -51,6 +52,7 @@ export const controllers = [
|
||||||
SharedLinkController,
|
SharedLinkController,
|
||||||
SyncController,
|
SyncController,
|
||||||
SystemConfigController,
|
SystemConfigController,
|
||||||
|
SystemMetadataController,
|
||||||
TagController,
|
TagController,
|
||||||
TimelineController,
|
TimelineController,
|
||||||
TrashController,
|
TrashController,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import {
|
import {
|
||||||
ServerConfigDto,
|
ServerConfigDto,
|
||||||
|
@ -65,11 +65,4 @@ export class ServerInfoController {
|
||||||
getSupportedMediaTypes(): ServerMediaTypesResponseDto {
|
getSupportedMediaTypes(): ServerMediaTypesResponseDto {
|
||||||
return this.service.getSupportedMediaTypes();
|
return this.service.getSupportedMediaTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@AdminRoute()
|
|
||||||
@Post('admin-onboarding')
|
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
|
||||||
setAdminOnboarding(): Promise<void> {
|
|
||||||
return this.service.setAdminOnboarding();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
28
server/src/controllers/system-metadata.controller.ts
Normal file
28
server/src/controllers/system-metadata.controller.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto';
|
||||||
|
import { Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
||||||
|
|
||||||
|
@ApiTags('System Metadata')
|
||||||
|
@Controller('system-metadata')
|
||||||
|
@Authenticated({ admin: true })
|
||||||
|
export class SystemMetadataController {
|
||||||
|
constructor(private service: SystemMetadataService) {}
|
||||||
|
|
||||||
|
@Get('admin-onboarding')
|
||||||
|
getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> {
|
||||||
|
return this.service.getAdminOnboarding();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('admin-onboarding')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> {
|
||||||
|
return this.service.updateAdminOnboarding(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('reverse-geocoding-state')
|
||||||
|
getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
|
||||||
|
return this.service.getReverseGeocodingState();
|
||||||
|
}
|
||||||
|
}
|
15
server/src/dtos/system-metadata.dto.ts
Normal file
15
server/src/dtos/system-metadata.dto.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class AdminOnboardingUpdateDto {
|
||||||
|
@IsBoolean()
|
||||||
|
isOnboarded!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminOnboardingResponseDto {
|
||||||
|
isOnboarded!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReverseGeocodingStateResponseDto {
|
||||||
|
lastUpdate!: string | null;
|
||||||
|
lastImportFileName!: string | null;
|
||||||
|
}
|
|
@ -36,8 +36,8 @@ export class MetadataRepository implements IMetadataRepository {
|
||||||
this.logger.log('Initializing metadata repository');
|
this.logger.log('Initializing metadata repository');
|
||||||
const geodataDate = await readFile(geodataDatePath, 'utf8');
|
const geodataDate = await readFile(geodataDatePath, 'utf8');
|
||||||
|
|
||||||
|
// TODO move to metadata service init
|
||||||
const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
|
const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
|
||||||
|
|
||||||
if (geocodingMetadata?.lastUpdate === geodataDate) {
|
if (geocodingMetadata?.lastUpdate === geodataDate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||||
import { StorageService } from 'src/services/storage.service';
|
import { StorageService } from 'src/services/storage.service';
|
||||||
import { SyncService } from 'src/services/sync.service';
|
import { SyncService } from 'src/services/sync.service';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
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';
|
||||||
|
@ -58,6 +59,7 @@ export const services = [
|
||||||
StorageTemplateService,
|
StorageTemplateService,
|
||||||
SyncService,
|
SyncService,
|
||||||
SystemConfigService,
|
SystemConfigService,
|
||||||
|
SystemMetadataService,
|
||||||
TagService,
|
TagService,
|
||||||
TimelineService,
|
TimelineService,
|
||||||
TrashService,
|
TrashService,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { serverVersion } from 'src/constants';
|
import { serverVersion } from 'src/constants';
|
||||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
|
@ -207,13 +206,6 @@ describe(ServerInfoService.name, () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setAdminOnboarding', () => {
|
|
||||||
it('should set admin onboarding to true', async () => {
|
|
||||||
await sut.setAdminOnboarding();
|
|
||||||
expect(systemMetadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getStats', () => {
|
describe('getStats', () => {
|
||||||
it('should total up usage by user', async () => {
|
it('should total up usage by user', async () => {
|
||||||
userMock.getUserStats.mockResolvedValue([
|
userMock.getUserStats.mockResolvedValue([
|
||||||
|
|
|
@ -51,7 +51,9 @@ export class ServerInfoService {
|
||||||
|
|
||||||
const featureFlags = await this.getFeatures();
|
const featureFlags = await this.getFeatures();
|
||||||
if (featureFlags.configFile) {
|
if (featureFlags.configFile) {
|
||||||
await this.setAdminOnboarding();
|
await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
|
||||||
|
isOnboarded: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,10 +107,6 @@ export class ServerInfoService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setAdminOnboarding(): Promise<void> {
|
|
||||||
return this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStatistics(): Promise<ServerStatsResponseDto> {
|
async getStatistics(): Promise<ServerStatsResponseDto> {
|
||||||
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
|
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
|
||||||
const serverStats = new ServerStatsResponseDto();
|
const serverStats = new ServerStatsResponseDto();
|
||||||
|
|
31
server/src/services/system-metadata.service.spec.ts
Normal file
31
server/src/services/system-metadata.service.spec.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
||||||
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
||||||
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
|
describe(SystemMetadataService.name, () => {
|
||||||
|
let sut: SystemMetadataService;
|
||||||
|
let metadataMock: Mocked<ISystemMetadataRepository>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
metadataMock = newSystemMetadataRepositoryMock();
|
||||||
|
sut = new SystemMetadataService(metadataMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work', () => {
|
||||||
|
expect(sut).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateAdminOnboarding', () => {
|
||||||
|
it('should update isOnboarded to true', async () => {
|
||||||
|
await expect(sut.updateAdminOnboarding({ isOnboarded: true })).resolves.toBeUndefined();
|
||||||
|
expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update isOnboarded to false', async () => {
|
||||||
|
await expect(sut.updateAdminOnboarding({ isOnboarded: false })).resolves.toBeUndefined();
|
||||||
|
expect(metadataMock.set).toHaveBeenCalledWith(SystemMetadataKey.ADMIN_ONBOARDING, { isOnboarded: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
29
server/src/services/system-metadata.service.ts
Normal file
29
server/src/services/system-metadata.service.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
AdminOnboardingResponseDto,
|
||||||
|
AdminOnboardingUpdateDto,
|
||||||
|
ReverseGeocodingStateResponseDto,
|
||||||
|
} from 'src/dtos/system-metadata.dto';
|
||||||
|
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemMetadataService {
|
||||||
|
constructor(@Inject(ISystemMetadataRepository) private repository: ISystemMetadataRepository) {}
|
||||||
|
|
||||||
|
async getAdminOnboarding(): Promise<AdminOnboardingResponseDto> {
|
||||||
|
const value = await this.repository.get(SystemMetadataKey.ADMIN_ONBOARDING);
|
||||||
|
return { isOnboarded: false, ...value };
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAdminOnboarding(dto: AdminOnboardingUpdateDto): Promise<void> {
|
||||||
|
await this.repository.set(SystemMetadataKey.ADMIN_ONBOARDING, {
|
||||||
|
isOnboarded: dto.isOnboarded,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
|
||||||
|
const value = await this.repository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
|
||||||
|
return { lastUpdate: null, lastImportFileName: null, ...value };
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte';
|
import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte';
|
||||||
import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte';
|
import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { setAdminOnboarding } from '@immich/sdk';
|
import { updateAdminOnboarding } from '@immich/sdk';
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
const handleDoneClicked = async () => {
|
const handleDoneClicked = async () => {
|
||||||
if (index >= onboardingSteps.length - 1) {
|
if (index >= onboardingSteps.length - 1) {
|
||||||
await setAdminOnboarding();
|
await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } });
|
||||||
await goto(AppRoute.PHOTOS);
|
await goto(AppRoute.PHOTOS);
|
||||||
} else {
|
} else {
|
||||||
index++;
|
index++;
|
||||||
|
|
Loading…
Reference in a new issue