mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Merge remote-tracking branch 'origin/main' into keynav_timeline
This commit is contained in:
commit
b9ff2a2e2e
43
.github/workflows/test.yml
vendored
43
.github/workflows/test.yml
vendored
@ -17,6 +17,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
should_run_i18n: ${{ steps.found_paths.outputs.i18n == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||
should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||
@ -36,6 +37,8 @@ jobs:
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
with:
|
||||
filters: |
|
||||
i18n:
|
||||
- 'i18n/**'
|
||||
web:
|
||||
- 'web/**'
|
||||
- 'i18n/**'
|
||||
@ -262,6 +265,46 @@ jobs:
|
||||
run: npm run test:cov
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
i18n-tests:
|
||||
name: Test i18n
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_i18n == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: './web/.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm --prefix=web ci
|
||||
|
||||
- name: Format
|
||||
run: npm --prefix=web run format:i18n
|
||||
|
||||
- name: Find file changes
|
||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
||||
id: verify-changed-files
|
||||
with:
|
||||
files: |
|
||||
i18n/**
|
||||
|
||||
- name: Verify files have not changed
|
||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
||||
env:
|
||||
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
|
||||
run: |
|
||||
echo "ERROR: i18n files not up to date!"
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
exit 1
|
||||
|
||||
e2e-tests-lint:
|
||||
name: End-to-End Lint
|
||||
needs: pre-job
|
||||
|
@ -121,6 +121,6 @@ Once this is done, you can continue to step 3 of "Basic Setup".
|
||||
|
||||
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
|
||||
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
|
||||
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
|
||||
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
|
||||
[jellyfin-lp]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#low-power-encoding
|
||||
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#known-issues-and-limitations-on-linux
|
||||
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
|
||||
|
@ -202,7 +202,6 @@ describe('/asset', () => {
|
||||
{
|
||||
name: 'Marie Curie',
|
||||
birthDate: null,
|
||||
thumbnailPath: '',
|
||||
isHidden: false,
|
||||
faces: [
|
||||
{
|
||||
@ -219,7 +218,6 @@ describe('/asset', () => {
|
||||
{
|
||||
name: 'Pierre Curie',
|
||||
birthDate: null,
|
||||
thumbnailPath: '',
|
||||
isHidden: false,
|
||||
faces: [
|
||||
{
|
||||
|
57
i18n/en.json
57
i18n/en.json
@ -1,17 +1,4 @@
|
||||
{
|
||||
"user_pin_code_settings": "PIN Code",
|
||||
"user_pin_code_settings_description": "Manage your PIN code",
|
||||
"current_pin_code": "Current PIN code",
|
||||
"new_pin_code": "New PIN code",
|
||||
"setup_pin_code": "Setup a PIN code",
|
||||
"confirm_new_pin_code": "Confirm new PIN code",
|
||||
"change_pin_code": "Change PIN code",
|
||||
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||
"pin_code_changed_successfully": "Successfully changed PIN code",
|
||||
"pin_code_setup_successfully": "Successfully setup a PIN code",
|
||||
"pin_code_reset_successfully": "Successfully reset PIN code",
|
||||
"reset_pin_code": "Reset PIN code",
|
||||
"about": "About",
|
||||
"account": "Account",
|
||||
"account_settings": "Account Settings",
|
||||
@ -39,6 +26,7 @@
|
||||
"add_to_album": "Add to album",
|
||||
"add_to_album_bottom_sheet_added": "Added to {album}",
|
||||
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
|
||||
"add_to_locked_folder": "Add to Locked Folder",
|
||||
"add_to_shared_album": "Add to shared album",
|
||||
"add_url": "Add URL",
|
||||
"added_to_archive": "Added to archive",
|
||||
@ -625,6 +613,7 @@
|
||||
"change_password_form_new_password": "New Password",
|
||||
"change_password_form_password_mismatch": "Passwords do not match",
|
||||
"change_password_form_reenter_new_password": "Re-enter New Password",
|
||||
"change_pin_code": "Change PIN code",
|
||||
"change_your_password": "Change your password",
|
||||
"changed_visibility_successfully": "Changed visibility successfully",
|
||||
"check_all": "Check All",
|
||||
@ -665,6 +654,7 @@
|
||||
"confirm_delete_face": "Are you sure you want to delete {name} face from the asset?",
|
||||
"confirm_delete_shared_link": "Are you sure you want to delete this shared link?",
|
||||
"confirm_keep_this_delete_others": "All other assets in the stack will be deleted except for this asset. Are you sure you want to continue?",
|
||||
"confirm_new_pin_code": "Confirm new PIN code",
|
||||
"confirm_password": "Confirm password",
|
||||
"contain": "Contain",
|
||||
"context": "Context",
|
||||
@ -707,9 +697,11 @@
|
||||
"create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.",
|
||||
"create_user": "Create user",
|
||||
"created": "Created",
|
||||
"created_at": "Created",
|
||||
"crop": "Crop",
|
||||
"curated_object_page_title": "Things",
|
||||
"current_device": "Current device",
|
||||
"current_pin_code": "Current PIN code",
|
||||
"current_server_address": "Current server address",
|
||||
"custom_locale": "Custom Locale",
|
||||
"custom_locale_description": "Format dates and numbers based on the language and the region",
|
||||
@ -822,6 +814,7 @@
|
||||
"editor_crop_tool_h2_aspect_ratios": "Aspect ratios",
|
||||
"editor_crop_tool_h2_rotation": "Rotation",
|
||||
"email": "Email",
|
||||
"email_notifications": "Email notifications",
|
||||
"empty_folder": "This folder is empty",
|
||||
"empty_trash": "Empty trash",
|
||||
"empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!",
|
||||
@ -830,6 +823,8 @@
|
||||
"end_date": "End date",
|
||||
"enqueued": "Enqueued",
|
||||
"enter_wifi_name": "Enter Wi-Fi name",
|
||||
"enter_your_pin_code": "Enter your PIN code",
|
||||
"enter_your_pin_code_subtitle": "Enter your PIN code to access the locked folder",
|
||||
"error": "Error",
|
||||
"error_change_sort_album": "Failed to change album sort order",
|
||||
"error_delete_face": "Error deleting face from asset",
|
||||
@ -924,6 +919,7 @@
|
||||
"unable_to_log_out_all_devices": "Unable to log out all devices",
|
||||
"unable_to_log_out_device": "Unable to log out device",
|
||||
"unable_to_login_with_oauth": "Unable to login with OAuth",
|
||||
"unable_to_move_to_locked_folder": "Unable to move to locked folder",
|
||||
"unable_to_play_video": "Unable to play video",
|
||||
"unable_to_reassign_assets_existing_person": "Unable to reassign assets to {name, select, null {an existing person} other {{name}}}",
|
||||
"unable_to_reassign_assets_new_person": "Unable to reassign assets to a new person",
|
||||
@ -1064,6 +1060,7 @@
|
||||
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
||||
"host": "Host",
|
||||
"hour": "Hour",
|
||||
"id": "ID",
|
||||
"ignore_icloud_photos": "Ignore iCloud photos",
|
||||
"ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
|
||||
"image": "Image",
|
||||
@ -1145,6 +1142,8 @@
|
||||
"location_picker_latitude_hint": "Enter your latitude here",
|
||||
"location_picker_longitude_error": "Enter a valid longitude",
|
||||
"location_picker_longitude_hint": "Enter your longitude here",
|
||||
"lock": "Lock",
|
||||
"locked_folder": "Locked Folder",
|
||||
"log_out": "Log out",
|
||||
"log_out_all_devices": "Log Out All Devices",
|
||||
"logged_out_all_devices": "Logged out all devices",
|
||||
@ -1213,8 +1212,8 @@
|
||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||
"map_settings_theme_settings": "Map Theme",
|
||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||
"mark_as_read": "Mark as read",
|
||||
"mark_all_as_read": "Mark all as read",
|
||||
"mark_as_read": "Mark as read",
|
||||
"marked_all_as_read": "Marked all as read",
|
||||
"matches": "Matches",
|
||||
"media_type": "Media type",
|
||||
@ -1242,6 +1241,10 @@
|
||||
"month": "Month",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"more": "More",
|
||||
"move": "Move",
|
||||
"move_off_locked_folder": "Move out of Locked Folder",
|
||||
"move_to_locked_folder": "Move to Locked Folder",
|
||||
"move_to_locked_folder_confirmation": "These photos and video will be removed from all albums, and only viewable from the Locked Folder",
|
||||
"moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
|
||||
"moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library",
|
||||
"moved_to_trash": "Moved to trash",
|
||||
@ -1258,6 +1261,8 @@
|
||||
"new_api_key": "New API Key",
|
||||
"new_password": "New password",
|
||||
"new_person": "New person",
|
||||
"new_pin_code": "New PIN code",
|
||||
"new_pin_code_subtitle": "This is your first time accessing the locked folder. Create a PIN code to securely access this page",
|
||||
"new_user_created": "New user created",
|
||||
"new_version_available": "NEW VERSION AVAILABLE",
|
||||
"newest_first": "Newest first",
|
||||
@ -1275,23 +1280,24 @@
|
||||
"no_explore_results_message": "Upload more photos to explore your collection.",
|
||||
"no_favorites_message": "Add favorites to quickly find your best pictures and videos",
|
||||
"no_libraries_message": "Create an external library to view your photos and videos",
|
||||
"no_locked_photos_message": "Photos and videos in Locked Folder are hidden and won't show up as you browser your library.",
|
||||
"no_name": "No Name",
|
||||
"no_notifications": "No notifications",
|
||||
"no_people_found": "No matching people found",
|
||||
"no_places": "No places",
|
||||
"no_results": "No results",
|
||||
"no_results_description": "Try a synonym or more general keyword",
|
||||
"no_notifications": "No notifications",
|
||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||
"not_in_any_album": "Not in any album",
|
||||
"not_selected": "Not selected",
|
||||
"note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||
"notes": "Notes",
|
||||
"nothing_here_yet": "Nothing here yet",
|
||||
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
||||
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
|
||||
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
||||
"notification_permission_list_tile_title": "Notification Permission",
|
||||
"notification_toggle_setting_description": "Enable email notifications",
|
||||
"email_notifications": "Email notifications",
|
||||
"notifications": "Notifications",
|
||||
"notifications_setting_description": "Manage notifications",
|
||||
"oauth": "OAuth",
|
||||
@ -1379,6 +1385,10 @@
|
||||
"photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}",
|
||||
"photos_from_previous_years": "Photos from previous years",
|
||||
"pick_a_location": "Pick a location",
|
||||
"pin_code_changed_successfully": "Successfully changed PIN code",
|
||||
"pin_code_reset_successfully": "Successfully reset PIN code",
|
||||
"pin_code_setup_successfully": "Successfully setup a PIN code",
|
||||
"pin_verification": "PIN code verification",
|
||||
"place": "Place",
|
||||
"places": "Places",
|
||||
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
|
||||
@ -1476,6 +1486,8 @@
|
||||
"remove_deleted_assets": "Remove Deleted Assets",
|
||||
"remove_from_album": "Remove from album",
|
||||
"remove_from_favorites": "Remove from favorites",
|
||||
"remove_from_locked_folder": "Remove from Locked Folder",
|
||||
"remove_from_locked_folder_confirmation": "Are you sure you want to move these photos and videos out of Locked Folder? They will be visible in your library",
|
||||
"remove_from_shared_link": "Remove from shared link",
|
||||
"remove_memory": "Remove memory",
|
||||
"remove_photo_from_memory": "Remove photo from this memory",
|
||||
@ -1499,6 +1511,7 @@
|
||||
"reset": "Reset",
|
||||
"reset_password": "Reset password",
|
||||
"reset_people_visibility": "Reset people visibility",
|
||||
"reset_pin_code": "Reset PIN code",
|
||||
"reset_to_default": "Reset to default",
|
||||
"resolve_duplicates": "Resolve duplicates",
|
||||
"resolved_all_duplicates": "Resolved all duplicates",
|
||||
@ -1639,6 +1652,7 @@
|
||||
"settings": "Settings",
|
||||
"settings_require_restart": "Please restart Immich to apply this setting",
|
||||
"settings_saved": "Settings saved",
|
||||
"setup_pin_code": "Setup a PIN code",
|
||||
"share": "Share",
|
||||
"share_add_photos": "Add photos",
|
||||
"share_assets_selected": "{count} selected",
|
||||
@ -1755,8 +1769,8 @@
|
||||
"stop_sharing_photos_with_user": "Stop sharing your photos with this user",
|
||||
"storage": "Storage space",
|
||||
"storage_label": "Storage label",
|
||||
"storage_usage": "{used} of {available} used",
|
||||
"storage_quota": "Storage Quota",
|
||||
"storage_usage": "{used} of {available} used",
|
||||
"submit": "Submit",
|
||||
"suggestions": "Suggestions",
|
||||
"sunrise_on_the_beach": "Sunrise on the beach",
|
||||
@ -1824,6 +1838,8 @@
|
||||
"trash_page_title": "Trash ({count})",
|
||||
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
||||
"type": "Type",
|
||||
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||
"unarchive": "Unarchive",
|
||||
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
||||
"unfavorite": "Unfavorite",
|
||||
@ -1847,6 +1863,7 @@
|
||||
"untracked_files": "Untracked files",
|
||||
"untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
|
||||
"up_next": "Up next",
|
||||
"updated_at": "Updated",
|
||||
"updated_password": "Updated password",
|
||||
"upload": "Upload",
|
||||
"upload_concurrency": "Upload concurrency",
|
||||
@ -1861,7 +1878,6 @@
|
||||
"upload_success": "Upload success, refresh the page to see new upload assets.",
|
||||
"upload_to_immich": "Upload to Immich ({count})",
|
||||
"uploading": "Uploading",
|
||||
"id": "ID",
|
||||
"url": "URL",
|
||||
"usage": "Usage",
|
||||
"use_current_connection": "use current connection",
|
||||
@ -1870,8 +1886,8 @@
|
||||
"user_has_been_deleted": "This user has been deleted.",
|
||||
"user_id": "User ID",
|
||||
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
||||
"created_at": "Created",
|
||||
"updated_at": "Updated",
|
||||
"user_pin_code_settings": "PIN Code",
|
||||
"user_pin_code_settings_description": "Manage your PIN code",
|
||||
"user_purchase_settings": "Purchase",
|
||||
"user_purchase_settings_description": "Manage your purchase",
|
||||
"user_role_set": "Set {user} as {role}",
|
||||
@ -1921,6 +1937,7 @@
|
||||
"welcome": "Welcome",
|
||||
"welcome_to_immich": "Welcome to Immich",
|
||||
"wifi_name": "Wi-Fi Name",
|
||||
"wrong_pin_code": "Wrong PIN code",
|
||||
"year": "Year",
|
||||
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
||||
"yes": "Yes",
|
||||
|
@ -29,6 +29,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||
case 'UserResponseDto':
|
||||
if (value is Map) {
|
||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||
addDefault(value, 'visibility', AssetVisibility.timeline);
|
||||
}
|
||||
break;
|
||||
case 'UserAdminResponseDto':
|
||||
|
8
mobile/openapi/README.md
generated
8
mobile/openapi/README.md
generated
@ -111,11 +111,13 @@ Class | Method | HTTP request | Description
|
||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code |
|
||||
*AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status |
|
||||
*AuthenticationApi* | [**lockAuthSession**](doc//AuthenticationApi.md#lockauthsession) | **POST** /auth/session/lock |
|
||||
*AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
||||
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
||||
*AuthenticationApi* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code |
|
||||
*AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /auth/pin-code |
|
||||
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||
*AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock |
|
||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
||||
@ -193,9 +195,11 @@ Class | Method | HTTP request | Description
|
||||
*ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history |
|
||||
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
|
||||
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
|
||||
*SessionsApi* | [**createSession**](doc//SessionsApi.md#createsession) | **POST** /sessions |
|
||||
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
|
||||
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
|
||||
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
|
||||
*SessionsApi* | [**lockSession**](doc//SessionsApi.md#locksession) | **POST** /sessions/{id}/lock |
|
||||
*SharedLinksApi* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets |
|
||||
*SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |
|
||||
*SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |
|
||||
@ -390,6 +394,7 @@ Class | Method | HTTP request | Description
|
||||
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
||||
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
|
||||
- [PinCodeChangeDto](doc//PinCodeChangeDto.md)
|
||||
- [PinCodeResetDto](doc//PinCodeResetDto.md)
|
||||
- [PinCodeSetupDto](doc//PinCodeSetupDto.md)
|
||||
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||
@ -419,7 +424,10 @@ Class | Method | HTTP request | Description
|
||||
- [ServerThemeDto](doc//ServerThemeDto.md)
|
||||
- [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md)
|
||||
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
||||
- [SessionCreateDto](doc//SessionCreateDto.md)
|
||||
- [SessionCreateResponseDto](doc//SessionCreateResponseDto.md)
|
||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
||||
- [SessionUnlockDto](doc//SessionUnlockDto.md)
|
||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||
|
4
mobile/openapi/lib/api.dart
generated
4
mobile/openapi/lib/api.dart
generated
@ -189,6 +189,7 @@ part 'model/person_statistics_response_dto.dart';
|
||||
part 'model/person_update_dto.dart';
|
||||
part 'model/person_with_faces_response_dto.dart';
|
||||
part 'model/pin_code_change_dto.dart';
|
||||
part 'model/pin_code_reset_dto.dart';
|
||||
part 'model/pin_code_setup_dto.dart';
|
||||
part 'model/places_response_dto.dart';
|
||||
part 'model/purchase_response.dart';
|
||||
@ -218,7 +219,10 @@ part 'model/server_storage_response_dto.dart';
|
||||
part 'model/server_theme_dto.dart';
|
||||
part 'model/server_version_history_response_dto.dart';
|
||||
part 'model/server_version_response_dto.dart';
|
||||
part 'model/session_create_dto.dart';
|
||||
part 'model/session_create_response_dto.dart';
|
||||
part 'model/session_response_dto.dart';
|
||||
part 'model/session_unlock_dto.dart';
|
||||
part 'model/shared_link_create_dto.dart';
|
||||
part 'model/shared_link_edit_dto.dart';
|
||||
part 'model/shared_link_response_dto.dart';
|
||||
|
84
mobile/openapi/lib/api/authentication_api.dart
generated
84
mobile/openapi/lib/api/authentication_api.dart
generated
@ -143,6 +143,39 @@ class AuthenticationApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /auth/session/lock' operation and returns the [Response].
|
||||
Future<Response> lockAuthSessionWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/auth/session/lock';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> lockAuthSession() async {
|
||||
final response = await lockAuthSessionWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
@ -234,13 +267,13 @@ class AuthenticationApi {
|
||||
/// Performs an HTTP 'DELETE /auth/pin-code' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [PinCodeChangeDto] pinCodeChangeDto (required):
|
||||
Future<Response> resetPinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async {
|
||||
/// * [PinCodeResetDto] pinCodeResetDto (required):
|
||||
Future<Response> resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/auth/pin-code';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = pinCodeChangeDto;
|
||||
Object? postBody = pinCodeResetDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
@ -262,9 +295,9 @@ class AuthenticationApi {
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [PinCodeChangeDto] pinCodeChangeDto (required):
|
||||
Future<void> resetPinCode(PinCodeChangeDto pinCodeChangeDto,) async {
|
||||
final response = await resetPinCodeWithHttpInfo(pinCodeChangeDto,);
|
||||
/// * [PinCodeResetDto] pinCodeResetDto (required):
|
||||
Future<void> resetPinCode(PinCodeResetDto pinCodeResetDto,) async {
|
||||
final response = await resetPinCodeWithHttpInfo(pinCodeResetDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@ -356,6 +389,45 @@ class AuthenticationApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /auth/session/unlock' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SessionUnlockDto] sessionUnlockDto (required):
|
||||
Future<Response> unlockAuthSessionWithHttpInfo(SessionUnlockDto sessionUnlockDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/auth/session/unlock';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = sessionUnlockDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SessionUnlockDto] sessionUnlockDto (required):
|
||||
Future<void> unlockAuthSession(SessionUnlockDto sessionUnlockDto,) async {
|
||||
final response = await unlockAuthSessionWithHttpInfo(sessionUnlockDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response].
|
||||
Future<Response> validateAccessTokenWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
|
87
mobile/openapi/lib/api/sessions_api.dart
generated
87
mobile/openapi/lib/api/sessions_api.dart
generated
@ -16,6 +16,53 @@ class SessionsApi {
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'POST /sessions' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SessionCreateDto] sessionCreateDto (required):
|
||||
Future<Response> createSessionWithHttpInfo(SessionCreateDto sessionCreateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/sessions';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = sessionCreateDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SessionCreateDto] sessionCreateDto (required):
|
||||
Future<SessionCreateResponseDto?> createSession(SessionCreateDto sessionCreateDto,) async {
|
||||
final response = await createSessionWithHttpInfo(sessionCreateDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SessionCreateResponseDto',) as SessionCreateResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /sessions' operation and returns the [Response].
|
||||
Future<Response> deleteAllSessionsWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
@ -132,4 +179,44 @@ class SessionsApi {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /sessions/{id}/lock' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> lockSessionWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/sessions/{id}/lock'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<void> lockSession(String id,) async {
|
||||
final response = await lockSessionWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
mobile/openapi/lib/api_client.dart
generated
8
mobile/openapi/lib/api_client.dart
generated
@ -434,6 +434,8 @@ class ApiClient {
|
||||
return PersonWithFacesResponseDto.fromJson(value);
|
||||
case 'PinCodeChangeDto':
|
||||
return PinCodeChangeDto.fromJson(value);
|
||||
case 'PinCodeResetDto':
|
||||
return PinCodeResetDto.fromJson(value);
|
||||
case 'PinCodeSetupDto':
|
||||
return PinCodeSetupDto.fromJson(value);
|
||||
case 'PlacesResponseDto':
|
||||
@ -492,8 +494,14 @@ class ApiClient {
|
||||
return ServerVersionHistoryResponseDto.fromJson(value);
|
||||
case 'ServerVersionResponseDto':
|
||||
return ServerVersionResponseDto.fromJson(value);
|
||||
case 'SessionCreateDto':
|
||||
return SessionCreateDto.fromJson(value);
|
||||
case 'SessionCreateResponseDto':
|
||||
return SessionCreateResponseDto.fromJson(value);
|
||||
case 'SessionResponseDto':
|
||||
return SessionResponseDto.fromJson(value);
|
||||
case 'SessionUnlockDto':
|
||||
return SessionUnlockDto.fromJson(value);
|
||||
case 'SharedLinkCreateDto':
|
||||
return SharedLinkCreateDto.fromJson(value);
|
||||
case 'SharedLinkEditDto':
|
||||
|
94
mobile/openapi/lib/model/asset_response_dto.dart
generated
94
mobile/openapi/lib/model/asset_response_dto.dart
generated
@ -43,6 +43,7 @@ class AssetResponseDto {
|
||||
required this.type,
|
||||
this.unassignedFaces = const [],
|
||||
required this.updatedAt,
|
||||
required this.visibility,
|
||||
});
|
||||
|
||||
/// base64 encoded sha1 hash
|
||||
@ -132,6 +133,8 @@ class AssetResponseDto {
|
||||
|
||||
DateTime updatedAt;
|
||||
|
||||
AssetResponseDtoVisibilityEnum visibility;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||
other.checksum == checksum &&
|
||||
@ -163,7 +166,8 @@ class AssetResponseDto {
|
||||
other.thumbhash == thumbhash &&
|
||||
other.type == type &&
|
||||
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
|
||||
other.updatedAt == updatedAt;
|
||||
other.updatedAt == updatedAt &&
|
||||
other.visibility == visibility;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@ -197,10 +201,11 @@ class AssetResponseDto {
|
||||
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||
(type.hashCode) +
|
||||
(unassignedFaces.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
(updatedAt.hashCode) +
|
||||
(visibility.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
|
||||
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -270,6 +275,7 @@ class AssetResponseDto {
|
||||
json[r'type'] = this.type;
|
||||
json[r'unassignedFaces'] = this.unassignedFaces;
|
||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||
json[r'visibility'] = this.visibility;
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -312,6 +318,7 @@ class AssetResponseDto {
|
||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||
visibility: AssetResponseDtoVisibilityEnum.fromJson(json[r'visibility'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@ -378,6 +385,87 @@ class AssetResponseDto {
|
||||
'thumbhash',
|
||||
'type',
|
||||
'updatedAt',
|
||||
'visibility',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class AssetResponseDtoVisibilityEnum {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const AssetResponseDtoVisibilityEnum._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const archive = AssetResponseDtoVisibilityEnum._(r'archive');
|
||||
static const timeline = AssetResponseDtoVisibilityEnum._(r'timeline');
|
||||
static const hidden = AssetResponseDtoVisibilityEnum._(r'hidden');
|
||||
static const locked = AssetResponseDtoVisibilityEnum._(r'locked');
|
||||
|
||||
/// List of all possible values in this [enum][AssetResponseDtoVisibilityEnum].
|
||||
static const values = <AssetResponseDtoVisibilityEnum>[
|
||||
archive,
|
||||
timeline,
|
||||
hidden,
|
||||
locked,
|
||||
];
|
||||
|
||||
static AssetResponseDtoVisibilityEnum? fromJson(dynamic value) => AssetResponseDtoVisibilityEnumTypeTransformer().decode(value);
|
||||
|
||||
static List<AssetResponseDtoVisibilityEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetResponseDtoVisibilityEnum>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetResponseDtoVisibilityEnum.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [AssetResponseDtoVisibilityEnum] to String,
|
||||
/// and [decode] dynamic data back to [AssetResponseDtoVisibilityEnum].
|
||||
class AssetResponseDtoVisibilityEnumTypeTransformer {
|
||||
factory AssetResponseDtoVisibilityEnumTypeTransformer() => _instance ??= const AssetResponseDtoVisibilityEnumTypeTransformer._();
|
||||
|
||||
const AssetResponseDtoVisibilityEnumTypeTransformer._();
|
||||
|
||||
String encode(AssetResponseDtoVisibilityEnum data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a AssetResponseDtoVisibilityEnum.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
AssetResponseDtoVisibilityEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'archive': return AssetResponseDtoVisibilityEnum.archive;
|
||||
case r'timeline': return AssetResponseDtoVisibilityEnum.timeline;
|
||||
case r'hidden': return AssetResponseDtoVisibilityEnum.hidden;
|
||||
case r'locked': return AssetResponseDtoVisibilityEnum.locked;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [AssetResponseDtoVisibilityEnumTypeTransformer] instance.
|
||||
static AssetResponseDtoVisibilityEnumTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
|
||||
|
3
mobile/openapi/lib/model/asset_visibility.dart
generated
3
mobile/openapi/lib/model/asset_visibility.dart
generated
@ -26,12 +26,14 @@ class AssetVisibility {
|
||||
static const archive = AssetVisibility._(r'archive');
|
||||
static const timeline = AssetVisibility._(r'timeline');
|
||||
static const hidden = AssetVisibility._(r'hidden');
|
||||
static const locked = AssetVisibility._(r'locked');
|
||||
|
||||
/// List of all possible values in this [enum][AssetVisibility].
|
||||
static const values = <AssetVisibility>[
|
||||
archive,
|
||||
timeline,
|
||||
hidden,
|
||||
locked,
|
||||
];
|
||||
|
||||
static AssetVisibility? fromJson(dynamic value) => AssetVisibilityTypeTransformer().decode(value);
|
||||
@ -73,6 +75,7 @@ class AssetVisibilityTypeTransformer {
|
||||
case r'archive': return AssetVisibility.archive;
|
||||
case r'timeline': return AssetVisibility.timeline;
|
||||
case r'hidden': return AssetVisibility.hidden;
|
||||
case r'locked': return AssetVisibility.locked;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
@ -13,32 +13,70 @@ part of openapi.api;
|
||||
class AuthStatusResponseDto {
|
||||
/// Returns a new [AuthStatusResponseDto] instance.
|
||||
AuthStatusResponseDto({
|
||||
this.expiresAt,
|
||||
required this.isElevated,
|
||||
required this.password,
|
||||
required this.pinCode,
|
||||
this.pinExpiresAt,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? expiresAt;
|
||||
|
||||
bool isElevated;
|
||||
|
||||
bool password;
|
||||
|
||||
bool pinCode;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? pinExpiresAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.isElevated == isElevated &&
|
||||
other.password == password &&
|
||||
other.pinCode == pinCode;
|
||||
other.pinCode == pinCode &&
|
||||
other.pinExpiresAt == pinExpiresAt;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||
(isElevated.hashCode) +
|
||||
(password.hashCode) +
|
||||
(pinCode.hashCode);
|
||||
(pinCode.hashCode) +
|
||||
(pinExpiresAt == null ? 0 : pinExpiresAt!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AuthStatusResponseDto[password=$password, pinCode=$pinCode]';
|
||||
String toString() => 'AuthStatusResponseDto[expiresAt=$expiresAt, isElevated=$isElevated, password=$password, pinCode=$pinCode, pinExpiresAt=$pinExpiresAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.expiresAt != null) {
|
||||
json[r'expiresAt'] = this.expiresAt;
|
||||
} else {
|
||||
// json[r'expiresAt'] = null;
|
||||
}
|
||||
json[r'isElevated'] = this.isElevated;
|
||||
json[r'password'] = this.password;
|
||||
json[r'pinCode'] = this.pinCode;
|
||||
if (this.pinExpiresAt != null) {
|
||||
json[r'pinExpiresAt'] = this.pinExpiresAt;
|
||||
} else {
|
||||
// json[r'pinExpiresAt'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -51,8 +89,11 @@ class AuthStatusResponseDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AuthStatusResponseDto(
|
||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||
isElevated: mapValueOfType<bool>(json, r'isElevated')!,
|
||||
password: mapValueOfType<bool>(json, r'password')!,
|
||||
pinCode: mapValueOfType<bool>(json, r'pinCode')!,
|
||||
pinExpiresAt: mapValueOfType<String>(json, r'pinExpiresAt'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@ -100,6 +141,7 @@ class AuthStatusResponseDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'isElevated',
|
||||
'password',
|
||||
'pinCode',
|
||||
};
|
||||
|
6
mobile/openapi/lib/model/permission.dart
generated
6
mobile/openapi/lib/model/permission.dart
generated
@ -81,9 +81,11 @@ class Permission {
|
||||
static const personPeriodStatistics = Permission._(r'person.statistics');
|
||||
static const personPeriodMerge = Permission._(r'person.merge');
|
||||
static const personPeriodReassign = Permission._(r'person.reassign');
|
||||
static const sessionPeriodCreate = Permission._(r'session.create');
|
||||
static const sessionPeriodRead = Permission._(r'session.read');
|
||||
static const sessionPeriodUpdate = Permission._(r'session.update');
|
||||
static const sessionPeriodDelete = Permission._(r'session.delete');
|
||||
static const sessionPeriodLock = Permission._(r'session.lock');
|
||||
static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create');
|
||||
static const sharedLinkPeriodRead = Permission._(r'sharedLink.read');
|
||||
static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update');
|
||||
@ -166,9 +168,11 @@ class Permission {
|
||||
personPeriodStatistics,
|
||||
personPeriodMerge,
|
||||
personPeriodReassign,
|
||||
sessionPeriodCreate,
|
||||
sessionPeriodRead,
|
||||
sessionPeriodUpdate,
|
||||
sessionPeriodDelete,
|
||||
sessionPeriodLock,
|
||||
sharedLinkPeriodCreate,
|
||||
sharedLinkPeriodRead,
|
||||
sharedLinkPeriodUpdate,
|
||||
@ -286,9 +290,11 @@ class PermissionTypeTransformer {
|
||||
case r'person.statistics': return Permission.personPeriodStatistics;
|
||||
case r'person.merge': return Permission.personPeriodMerge;
|
||||
case r'person.reassign': return Permission.personPeriodReassign;
|
||||
case r'session.create': return Permission.sessionPeriodCreate;
|
||||
case r'session.read': return Permission.sessionPeriodRead;
|
||||
case r'session.update': return Permission.sessionPeriodUpdate;
|
||||
case r'session.delete': return Permission.sessionPeriodDelete;
|
||||
case r'session.lock': return Permission.sessionPeriodLock;
|
||||
case r'sharedLink.create': return Permission.sharedLinkPeriodCreate;
|
||||
case r'sharedLink.read': return Permission.sharedLinkPeriodRead;
|
||||
case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate;
|
||||
|
125
mobile/openapi/lib/model/pin_code_reset_dto.dart
generated
Normal file
125
mobile/openapi/lib/model/pin_code_reset_dto.dart
generated
Normal file
@ -0,0 +1,125 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class PinCodeResetDto {
|
||||
/// Returns a new [PinCodeResetDto] instance.
|
||||
PinCodeResetDto({
|
||||
this.password,
|
||||
this.pinCode,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? password;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? pinCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PinCodeResetDto &&
|
||||
other.password == password &&
|
||||
other.pinCode == pinCode;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(password == null ? 0 : password!.hashCode) +
|
||||
(pinCode == null ? 0 : pinCode!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PinCodeResetDto[password=$password, pinCode=$pinCode]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.password != null) {
|
||||
json[r'password'] = this.password;
|
||||
} else {
|
||||
// json[r'password'] = null;
|
||||
}
|
||||
if (this.pinCode != null) {
|
||||
json[r'pinCode'] = this.pinCode;
|
||||
} else {
|
||||
// json[r'pinCode'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PinCodeResetDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PinCodeResetDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "PinCodeResetDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PinCodeResetDto(
|
||||
password: mapValueOfType<String>(json, r'password'),
|
||||
pinCode: mapValueOfType<String>(json, r'pinCode'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PinCodeResetDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PinCodeResetDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PinCodeResetDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PinCodeResetDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PinCodeResetDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PinCodeResetDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PinCodeResetDto-objects as value to a dart map
|
||||
static Map<String, List<PinCodeResetDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PinCodeResetDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PinCodeResetDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
145
mobile/openapi/lib/model/session_create_dto.dart
generated
Normal file
145
mobile/openapi/lib/model/session_create_dto.dart
generated
Normal file
@ -0,0 +1,145 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SessionCreateDto {
|
||||
/// Returns a new [SessionCreateDto] instance.
|
||||
SessionCreateDto({
|
||||
this.deviceOS,
|
||||
this.deviceType,
|
||||
this.duration,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? deviceOS;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? deviceType;
|
||||
|
||||
/// session duration, in seconds
|
||||
///
|
||||
/// Minimum value: 1
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
num? duration;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto &&
|
||||
other.deviceOS == deviceOS &&
|
||||
other.deviceType == deviceType &&
|
||||
other.duration == duration;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(deviceOS == null ? 0 : deviceOS!.hashCode) +
|
||||
(deviceType == null ? 0 : deviceType!.hashCode) +
|
||||
(duration == null ? 0 : duration!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SessionCreateDto[deviceOS=$deviceOS, deviceType=$deviceType, duration=$duration]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.deviceOS != null) {
|
||||
json[r'deviceOS'] = this.deviceOS;
|
||||
} else {
|
||||
// json[r'deviceOS'] = null;
|
||||
}
|
||||
if (this.deviceType != null) {
|
||||
json[r'deviceType'] = this.deviceType;
|
||||
} else {
|
||||
// json[r'deviceType'] = null;
|
||||
}
|
||||
if (this.duration != null) {
|
||||
json[r'duration'] = this.duration;
|
||||
} else {
|
||||
// json[r'duration'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SessionCreateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SessionCreateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SessionCreateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SessionCreateDto(
|
||||
deviceOS: mapValueOfType<String>(json, r'deviceOS'),
|
||||
deviceType: mapValueOfType<String>(json, r'deviceType'),
|
||||
duration: num.parse('${json[r'duration']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SessionCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SessionCreateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SessionCreateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SessionCreateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SessionCreateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SessionCreateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SessionCreateDto-objects as value to a dart map
|
||||
static Map<String, List<SessionCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SessionCreateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SessionCreateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
164
mobile/openapi/lib/model/session_create_response_dto.dart
generated
Normal file
164
mobile/openapi/lib/model/session_create_response_dto.dart
generated
Normal file
@ -0,0 +1,164 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SessionCreateResponseDto {
|
||||
/// Returns a new [SessionCreateResponseDto] instance.
|
||||
SessionCreateResponseDto({
|
||||
required this.createdAt,
|
||||
required this.current,
|
||||
required this.deviceOS,
|
||||
required this.deviceType,
|
||||
this.expiresAt,
|
||||
required this.id,
|
||||
required this.token,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
String createdAt;
|
||||
|
||||
bool current;
|
||||
|
||||
String deviceOS;
|
||||
|
||||
String deviceType;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? expiresAt;
|
||||
|
||||
String id;
|
||||
|
||||
String token;
|
||||
|
||||
String updatedAt;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SessionCreateResponseDto &&
|
||||
other.createdAt == createdAt &&
|
||||
other.current == current &&
|
||||
other.deviceOS == deviceOS &&
|
||||
other.deviceType == deviceType &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.id == id &&
|
||||
other.token == token &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(createdAt.hashCode) +
|
||||
(current.hashCode) +
|
||||
(deviceOS.hashCode) +
|
||||
(deviceType.hashCode) +
|
||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(token.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'createdAt'] = this.createdAt;
|
||||
json[r'current'] = this.current;
|
||||
json[r'deviceOS'] = this.deviceOS;
|
||||
json[r'deviceType'] = this.deviceType;
|
||||
if (this.expiresAt != null) {
|
||||
json[r'expiresAt'] = this.expiresAt;
|
||||
} else {
|
||||
// json[r'expiresAt'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'token'] = this.token;
|
||||
json[r'updatedAt'] = this.updatedAt;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SessionCreateResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SessionCreateResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SessionCreateResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SessionCreateResponseDto(
|
||||
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
||||
current: mapValueOfType<bool>(json, r'current')!,
|
||||
deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
|
||||
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
token: mapValueOfType<String>(json, r'token')!,
|
||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SessionCreateResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SessionCreateResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SessionCreateResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SessionCreateResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SessionCreateResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SessionCreateResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SessionCreateResponseDto-objects as value to a dart map
|
||||
static Map<String, List<SessionCreateResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SessionCreateResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SessionCreateResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'createdAt',
|
||||
'current',
|
||||
'deviceOS',
|
||||
'deviceType',
|
||||
'id',
|
||||
'token',
|
||||
'updatedAt',
|
||||
};
|
||||
}
|
||||
|
19
mobile/openapi/lib/model/session_response_dto.dart
generated
19
mobile/openapi/lib/model/session_response_dto.dart
generated
@ -17,6 +17,7 @@ class SessionResponseDto {
|
||||
required this.current,
|
||||
required this.deviceOS,
|
||||
required this.deviceType,
|
||||
this.expiresAt,
|
||||
required this.id,
|
||||
required this.updatedAt,
|
||||
});
|
||||
@ -29,6 +30,14 @@ class SessionResponseDto {
|
||||
|
||||
String deviceType;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? expiresAt;
|
||||
|
||||
String id;
|
||||
|
||||
String updatedAt;
|
||||
@ -39,6 +48,7 @@ class SessionResponseDto {
|
||||
other.current == current &&
|
||||
other.deviceOS == deviceOS &&
|
||||
other.deviceType == deviceType &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.id == id &&
|
||||
other.updatedAt == updatedAt;
|
||||
|
||||
@ -49,11 +59,12 @@ class SessionResponseDto {
|
||||
(current.hashCode) +
|
||||
(deviceOS.hashCode) +
|
||||
(deviceType.hashCode) +
|
||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, id=$id, updatedAt=$updatedAt]';
|
||||
String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -61,6 +72,11 @@ class SessionResponseDto {
|
||||
json[r'current'] = this.current;
|
||||
json[r'deviceOS'] = this.deviceOS;
|
||||
json[r'deviceType'] = this.deviceType;
|
||||
if (this.expiresAt != null) {
|
||||
json[r'expiresAt'] = this.expiresAt;
|
||||
} else {
|
||||
// json[r'expiresAt'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'updatedAt'] = this.updatedAt;
|
||||
return json;
|
||||
@ -79,6 +95,7 @@ class SessionResponseDto {
|
||||
current: mapValueOfType<bool>(json, r'current')!,
|
||||
deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
|
||||
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||
);
|
||||
|
125
mobile/openapi/lib/model/session_unlock_dto.dart
generated
Normal file
125
mobile/openapi/lib/model/session_unlock_dto.dart
generated
Normal file
@ -0,0 +1,125 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SessionUnlockDto {
|
||||
/// Returns a new [SessionUnlockDto] instance.
|
||||
SessionUnlockDto({
|
||||
this.password,
|
||||
this.pinCode,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? password;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? pinCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SessionUnlockDto &&
|
||||
other.password == password &&
|
||||
other.pinCode == pinCode;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(password == null ? 0 : password!.hashCode) +
|
||||
(pinCode == null ? 0 : pinCode!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SessionUnlockDto[password=$password, pinCode=$pinCode]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.password != null) {
|
||||
json[r'password'] = this.password;
|
||||
} else {
|
||||
// json[r'password'] = null;
|
||||
}
|
||||
if (this.pinCode != null) {
|
||||
json[r'pinCode'] = this.pinCode;
|
||||
} else {
|
||||
// json[r'pinCode'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SessionUnlockDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SessionUnlockDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SessionUnlockDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SessionUnlockDto(
|
||||
password: mapValueOfType<String>(json, r'password'),
|
||||
pinCode: mapValueOfType<String>(json, r'pinCode'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SessionUnlockDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SessionUnlockDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SessionUnlockDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SessionUnlockDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SessionUnlockDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SessionUnlockDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SessionUnlockDto-objects as value to a dart map
|
||||
static Map<String, List<SessionUnlockDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SessionUnlockDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SessionUnlockDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
};
|
||||
}
|
||||
|
3
mobile/openapi/lib/model/sync_asset_v1.dart
generated
3
mobile/openapi/lib/model/sync_asset_v1.dart
generated
@ -293,12 +293,14 @@ class SyncAssetV1VisibilityEnum {
|
||||
static const archive = SyncAssetV1VisibilityEnum._(r'archive');
|
||||
static const timeline = SyncAssetV1VisibilityEnum._(r'timeline');
|
||||
static const hidden = SyncAssetV1VisibilityEnum._(r'hidden');
|
||||
static const locked = SyncAssetV1VisibilityEnum._(r'locked');
|
||||
|
||||
/// List of all possible values in this [enum][SyncAssetV1VisibilityEnum].
|
||||
static const values = <SyncAssetV1VisibilityEnum>[
|
||||
archive,
|
||||
timeline,
|
||||
hidden,
|
||||
locked,
|
||||
];
|
||||
|
||||
static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value);
|
||||
@ -340,6 +342,7 @@ class SyncAssetV1VisibilityEnumTypeTransformer {
|
||||
case r'archive': return SyncAssetV1VisibilityEnum.archive;
|
||||
case r'timeline': return SyncAssetV1VisibilityEnum.timeline;
|
||||
case r'hidden': return SyncAssetV1VisibilityEnum.hidden;
|
||||
case r'locked': return SyncAssetV1VisibilityEnum.locked;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
@ -2377,7 +2377,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PinCodeChangeDto"
|
||||
"$ref": "#/components/schemas/PinCodeResetDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -2470,6 +2470,66 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/auth/session/lock": {
|
||||
"post": {
|
||||
"operationId": "lockAuthSession",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/auth/session/unlock": {
|
||||
"post": {
|
||||
"operationId": "unlockAuthSession",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SessionUnlockDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Authentication"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/auth/status": {
|
||||
"get": {
|
||||
"operationId": "getAuthStatus",
|
||||
@ -5583,6 +5643,46 @@
|
||||
"tags": [
|
||||
"Sessions"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createSession",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SessionCreateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SessionCreateResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Sessions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{id}": {
|
||||
@ -5620,6 +5720,41 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/sessions/{id}/lock": {
|
||||
"post": {
|
||||
"operationId": "lockSession",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Sessions"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/shared-links": {
|
||||
"get": {
|
||||
"operationId": "getAllSharedLinks",
|
||||
@ -9150,6 +9285,15 @@
|
||||
"updatedAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"visibility": {
|
||||
"enum": [
|
||||
"archive",
|
||||
"timeline",
|
||||
"hidden",
|
||||
"locked"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -9171,7 +9315,8 @@
|
||||
"ownerId",
|
||||
"thumbhash",
|
||||
"type",
|
||||
"updatedAt"
|
||||
"updatedAt",
|
||||
"visibility"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@ -9226,7 +9371,8 @@
|
||||
"enum": [
|
||||
"archive",
|
||||
"timeline",
|
||||
"hidden"
|
||||
"hidden",
|
||||
"locked"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@ -9241,14 +9387,24 @@
|
||||
},
|
||||
"AuthStatusResponseDto": {
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"isElevated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"password": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"pinCode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"pinExpiresAt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"isElevated",
|
||||
"password",
|
||||
"pinCode"
|
||||
],
|
||||
@ -11002,9 +11158,11 @@
|
||||
"person.statistics",
|
||||
"person.merge",
|
||||
"person.reassign",
|
||||
"session.create",
|
||||
"session.read",
|
||||
"session.update",
|
||||
"session.delete",
|
||||
"session.lock",
|
||||
"sharedLink.create",
|
||||
"sharedLink.read",
|
||||
"sharedLink.update",
|
||||
@ -11206,6 +11364,18 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PinCodeResetDto": {
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"pinCode": {
|
||||
"example": "123456",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PinCodeSetupDto": {
|
||||
"properties": {
|
||||
"pinCode": {
|
||||
@ -11988,6 +12158,60 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SessionCreateDto": {
|
||||
"properties": {
|
||||
"deviceOS": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"description": "session duration, in seconds",
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SessionCreateResponseDto": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"current": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"deviceOS": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"expiresAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"current",
|
||||
"deviceOS",
|
||||
"deviceType",
|
||||
"id",
|
||||
"token",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SessionResponseDto": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
@ -12002,6 +12226,9 @@
|
||||
"deviceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"expiresAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
@ -12019,6 +12246,18 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SessionUnlockDto": {
|
||||
"properties": {
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"pinCode": {
|
||||
"example": "123456",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SharedLinkCreateDto": {
|
||||
"properties": {
|
||||
"albumId": {
|
||||
@ -12664,7 +12903,8 @@
|
||||
"enum": [
|
||||
"archive",
|
||||
"timeline",
|
||||
"hidden"
|
||||
"hidden",
|
||||
"locked"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
|
@ -329,6 +329,7 @@ export type AssetResponseDto = {
|
||||
"type": AssetTypeEnum;
|
||||
unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
|
||||
updatedAt: string;
|
||||
visibility: Visibility;
|
||||
};
|
||||
export type AlbumResponseDto = {
|
||||
albumName: string;
|
||||
@ -511,17 +512,28 @@ export type LogoutResponseDto = {
|
||||
redirectUri: string;
|
||||
successful: boolean;
|
||||
};
|
||||
export type PinCodeChangeDto = {
|
||||
newPinCode: string;
|
||||
export type PinCodeResetDto = {
|
||||
password?: string;
|
||||
pinCode?: string;
|
||||
};
|
||||
export type PinCodeSetupDto = {
|
||||
pinCode: string;
|
||||
};
|
||||
export type PinCodeChangeDto = {
|
||||
newPinCode: string;
|
||||
password?: string;
|
||||
pinCode?: string;
|
||||
};
|
||||
export type SessionUnlockDto = {
|
||||
password?: string;
|
||||
pinCode?: string;
|
||||
};
|
||||
export type AuthStatusResponseDto = {
|
||||
expiresAt?: string;
|
||||
isElevated: boolean;
|
||||
password: boolean;
|
||||
pinCode: boolean;
|
||||
pinExpiresAt?: string;
|
||||
};
|
||||
export type ValidateAccessTokenResponseDto = {
|
||||
authStatus: boolean;
|
||||
@ -1073,9 +1085,26 @@ export type SessionResponseDto = {
|
||||
current: boolean;
|
||||
deviceOS: string;
|
||||
deviceType: string;
|
||||
expiresAt?: string;
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
export type SessionCreateDto = {
|
||||
deviceOS?: string;
|
||||
deviceType?: string;
|
||||
/** session duration, in seconds */
|
||||
duration?: number;
|
||||
};
|
||||
export type SessionCreateResponseDto = {
|
||||
createdAt: string;
|
||||
current: boolean;
|
||||
deviceOS: string;
|
||||
deviceType: string;
|
||||
expiresAt?: string;
|
||||
id: string;
|
||||
token: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
export type SharedLinkResponseDto = {
|
||||
album?: AlbumResponseDto;
|
||||
allowDownload: boolean;
|
||||
@ -2049,13 +2078,13 @@ export function logout(opts?: Oazapfts.RequestOpts) {
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function resetPinCode({ pinCodeChangeDto }: {
|
||||
pinCodeChangeDto: PinCodeChangeDto;
|
||||
export function resetPinCode({ pinCodeResetDto }: {
|
||||
pinCodeResetDto: PinCodeResetDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
|
||||
...opts,
|
||||
method: "DELETE",
|
||||
body: pinCodeChangeDto
|
||||
body: pinCodeResetDto
|
||||
})));
|
||||
}
|
||||
export function setupPinCode({ pinCodeSetupDto }: {
|
||||
@ -2076,6 +2105,21 @@ export function changePinCode({ pinCodeChangeDto }: {
|
||||
body: pinCodeChangeDto
|
||||
})));
|
||||
}
|
||||
export function lockAuthSession(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/auth/session/lock", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function unlockAuthSession({ sessionUnlockDto }: {
|
||||
sessionUnlockDto: SessionUnlockDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/auth/session/unlock", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: sessionUnlockDto
|
||||
})));
|
||||
}
|
||||
export function getAuthStatus(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
@ -2906,6 +2950,18 @@ export function getSessions(opts?: Oazapfts.RequestOpts) {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function createSession({ sessionCreateDto }: {
|
||||
sessionCreateDto: SessionCreateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: SessionCreateResponseDto;
|
||||
}>("/sessions", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: sessionCreateDto
|
||||
})));
|
||||
}
|
||||
export function deleteSession({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@ -2914,6 +2970,14 @@ export function deleteSession({ id }: {
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function lockSession({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/sessions/${encodeURIComponent(id)}/lock`, {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
export function getAllSharedLinks({ albumId }: {
|
||||
albumId?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@ -3574,7 +3638,8 @@ export enum UserStatus {
|
||||
export enum AssetVisibility {
|
||||
Archive = "archive",
|
||||
Timeline = "timeline",
|
||||
Hidden = "hidden"
|
||||
Hidden = "hidden",
|
||||
Locked = "locked"
|
||||
}
|
||||
export enum AlbumUserRole {
|
||||
Editor = "editor",
|
||||
@ -3591,6 +3656,12 @@ export enum AssetTypeEnum {
|
||||
Audio = "AUDIO",
|
||||
Other = "OTHER"
|
||||
}
|
||||
export enum Visibility {
|
||||
Archive = "archive",
|
||||
Timeline = "timeline",
|
||||
Hidden = "hidden",
|
||||
Locked = "locked"
|
||||
}
|
||||
export enum AssetOrder {
|
||||
Asc = "asc",
|
||||
Desc = "desc"
|
||||
@ -3660,9 +3731,11 @@ export enum Permission {
|
||||
PersonStatistics = "person.statistics",
|
||||
PersonMerge = "person.merge",
|
||||
PersonReassign = "person.reassign",
|
||||
SessionCreate = "session.create",
|
||||
SessionRead = "session.read",
|
||||
SessionUpdate = "session.update",
|
||||
SessionDelete = "session.delete",
|
||||
SessionLock = "session.lock",
|
||||
SharedLinkCreate = "sharedLink.create",
|
||||
SharedLinkRead = "sharedLink.read",
|
||||
SharedLinkUpdate = "sharedLink.update",
|
||||
|
@ -9,7 +9,9 @@ import {
|
||||
LoginResponseDto,
|
||||
LogoutResponseDto,
|
||||
PinCodeChangeDto,
|
||||
PinCodeResetDto,
|
||||
PinCodeSetupDto,
|
||||
SessionUnlockDto,
|
||||
SignUpDto,
|
||||
ValidateAccessTokenResponseDto,
|
||||
} from 'src/dtos/auth.dto';
|
||||
@ -98,7 +100,21 @@ export class AuthController {
|
||||
|
||||
@Delete('pin-code')
|
||||
@Authenticated()
|
||||
async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> {
|
||||
async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise<void> {
|
||||
return this.service.resetPinCode(auth, dto);
|
||||
}
|
||||
|
||||
@Post('session/unlock')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
async unlockAuthSession(@Auth() auth: AuthDto, @Body() dto: SessionUnlockDto): Promise<void> {
|
||||
return this.service.unlockSession(auth, dto);
|
||||
}
|
||||
|
||||
@Post('session/lock')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
async lockAuthSession(@Auth() auth: AuthDto): Promise<void> {
|
||||
return this.service.lockSession(auth);
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ describe(SearchController.name, () => {
|
||||
.send({ visibility: 'immich' });
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden']),
|
||||
errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden, locked']),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Controller, Delete, Get, HttpCode, HttpStatus, Param } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionResponseDto } from 'src/dtos/session.dto';
|
||||
import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto } from 'src/dtos/session.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { SessionService } from 'src/services/session.service';
|
||||
@ -12,6 +12,12 @@ import { UUIDParamDto } from 'src/validation';
|
||||
export class SessionController {
|
||||
constructor(private service: SessionService) {}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ permission: Permission.SESSION_CREATE })
|
||||
createSession(@Auth() auth: AuthDto, @Body() dto: SessionCreateDto): Promise<SessionCreateResponseDto> {
|
||||
return this.service.create(auth, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.SESSION_READ })
|
||||
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
|
||||
@ -31,4 +37,11 @@ export class SessionController {
|
||||
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.delete(auth, id);
|
||||
}
|
||||
|
||||
@Post(':id/lock')
|
||||
@Authenticated({ permission: Permission.SESSION_LOCK })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||
return this.service.lock(auth, id);
|
||||
}
|
||||
}
|
||||
|
@ -200,6 +200,7 @@ export type Album = Selectable<Albums> & {
|
||||
|
||||
export type AuthSession = {
|
||||
id: string;
|
||||
hasElevatedPermission: boolean;
|
||||
};
|
||||
|
||||
export type Partner = {
|
||||
@ -231,8 +232,10 @@ export type Session = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
expiresAt: Date | null;
|
||||
deviceOS: string;
|
||||
deviceType: string;
|
||||
pinExpiresAt: Date | null;
|
||||
};
|
||||
|
||||
export type Exif = Omit<Selectable<DatabaseExif>, 'updatedAt' | 'updateId'>;
|
||||
@ -306,7 +309,7 @@ export const columns = {
|
||||
'users.quotaSizeInBytes',
|
||||
],
|
||||
authApiKey: ['api_keys.id', 'api_keys.permissions'],
|
||||
authSession: ['sessions.id', 'sessions.updatedAt'],
|
||||
authSession: ['sessions.id', 'sessions.updatedAt', 'sessions.pinExpiresAt'],
|
||||
authSharedLink: [
|
||||
'shared_links.id',
|
||||
'shared_links.userId',
|
||||
|
3
server/src/db.d.ts
vendored
3
server/src/db.d.ts
vendored
@ -343,10 +343,13 @@ export interface Sessions {
|
||||
deviceOS: Generated<string>;
|
||||
deviceType: Generated<string>;
|
||||
id: Generated<string>;
|
||||
parentId: string | null;
|
||||
expiresAt: Date | null;
|
||||
token: string;
|
||||
updatedAt: Generated<Timestamp>;
|
||||
updateId: Generated<string>;
|
||||
userId: string;
|
||||
pinExpiresAt: Timestamp | null;
|
||||
}
|
||||
|
||||
export interface SessionSyncCheckpoints {
|
||||
|
@ -43,6 +43,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||
isArchived!: boolean;
|
||||
isTrashed!: boolean;
|
||||
isOffline!: boolean;
|
||||
visibility!: AssetVisibility;
|
||||
exifInfo?: ExifResponseDto;
|
||||
tags?: TagResponseDto[];
|
||||
people?: PersonWithFacesResponseDto[];
|
||||
@ -184,6 +185,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
||||
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
|
||||
isArchived: entity.visibility === AssetVisibility.ARCHIVE,
|
||||
isTrashed: !!entity.deletedAt,
|
||||
visibility: entity.visibility,
|
||||
duration: entity.duration ?? '0:00:00.00000',
|
||||
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
||||
livePhotoVideoId: entity.livePhotoVideoId,
|
||||
|
@ -93,6 +93,8 @@ export class PinCodeResetDto {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class SessionUnlockDto extends PinCodeResetDto {}
|
||||
|
||||
export class PinCodeChangeDto extends PinCodeResetDto {
|
||||
@PinCode()
|
||||
newPinCode!: string;
|
||||
@ -138,4 +140,7 @@ export class OAuthAuthorizeResponseDto {
|
||||
export class AuthStatusResponseDto {
|
||||
pinCode!: boolean;
|
||||
password!: boolean;
|
||||
isElevated!: boolean;
|
||||
expiresAt?: string;
|
||||
pinExpiresAt?: string;
|
||||
}
|
||||
|
@ -1,18 +1,44 @@
|
||||
import { IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { Session } from 'src/database';
|
||||
import { Optional } from 'src/validation';
|
||||
|
||||
export class SessionCreateDto {
|
||||
/**
|
||||
* session duration, in seconds
|
||||
*/
|
||||
@IsInt()
|
||||
@IsPositive()
|
||||
@Optional()
|
||||
duration?: number;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
deviceType?: string;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
deviceOS?: string;
|
||||
}
|
||||
|
||||
export class SessionResponseDto {
|
||||
id!: string;
|
||||
createdAt!: string;
|
||||
updatedAt!: string;
|
||||
expiresAt?: string;
|
||||
current!: boolean;
|
||||
deviceType!: string;
|
||||
deviceOS!: string;
|
||||
}
|
||||
|
||||
export class SessionCreateResponseDto extends SessionResponseDto {
|
||||
token!: string;
|
||||
}
|
||||
|
||||
export const mapSession = (entity: Session, currentId?: string): SessionResponseDto => ({
|
||||
id: entity.id,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
expiresAt: entity.expiresAt?.toISOString(),
|
||||
current: currentId === entity.id,
|
||||
deviceOS: entity.deviceOS,
|
||||
deviceType: entity.deviceType,
|
||||
|
@ -144,9 +144,11 @@ export enum Permission {
|
||||
PERSON_MERGE = 'person.merge',
|
||||
PERSON_REASSIGN = 'person.reassign',
|
||||
|
||||
SESSION_CREATE = 'session.create',
|
||||
SESSION_READ = 'session.read',
|
||||
SESSION_UPDATE = 'session.update',
|
||||
SESSION_DELETE = 'session.delete',
|
||||
SESSION_LOCK = 'session.lock',
|
||||
|
||||
SHARED_LINK_CREATE = 'sharedLink.create',
|
||||
SHARED_LINK_READ = 'sharedLink.read',
|
||||
@ -627,4 +629,5 @@ export enum AssetVisibility {
|
||||
* Video part of the LivePhotos and MotionPhotos
|
||||
*/
|
||||
HIDDEN = 'hidden',
|
||||
LOCKED = 'locked',
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ from
|
||||
where
|
||||
"assets"."id" in ($1)
|
||||
and "assets"."ownerId" = $2
|
||||
and "assets"."visibility" != $3
|
||||
|
||||
-- AccessRepository.asset.checkPartnerAccess
|
||||
select
|
||||
@ -198,6 +199,15 @@ where
|
||||
"partners"."sharedById" in ($1)
|
||||
and "partners"."sharedWithId" = $2
|
||||
|
||||
-- AccessRepository.session.checkOwnerAccess
|
||||
select
|
||||
"sessions"."id"
|
||||
from
|
||||
"sessions"
|
||||
where
|
||||
"sessions"."id" in ($1)
|
||||
and "sessions"."userId" = $2
|
||||
|
||||
-- AccessRepository.stack.checkOwnerAccess
|
||||
select
|
||||
"stacks"."id"
|
||||
|
@ -392,6 +392,11 @@ where
|
||||
order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.removeAssetsFromAll
|
||||
delete from "albums_assets_assets"
|
||||
where
|
||||
"albums_assets_assets"."assetsId" in ($1)
|
||||
|
||||
-- AlbumRepository.getAssetIds
|
||||
select
|
||||
*
|
||||
|
@ -432,3 +432,34 @@ where
|
||||
and "assets"."updatedAt" > $3
|
||||
limit
|
||||
$4
|
||||
|
||||
-- AssetRepository.detectOfflineExternalAssets
|
||||
update "assets"
|
||||
set
|
||||
"isOffline" = $1,
|
||||
"deletedAt" = $2
|
||||
where
|
||||
"isOffline" = $3
|
||||
and "isExternal" = $4
|
||||
and "libraryId" = $5::uuid
|
||||
and (
|
||||
not "originalPath" like $6
|
||||
or "originalPath" like $7
|
||||
)
|
||||
|
||||
-- AssetRepository.filterNewExternalAssetPaths
|
||||
select
|
||||
"path"
|
||||
from
|
||||
unnest(array[$1]::text[]) as "path"
|
||||
where
|
||||
not exists (
|
||||
select
|
||||
"originalPath"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."originalPath" = "path"
|
||||
and "libraryId" = $2::uuid
|
||||
and "isExternal" = $3
|
||||
)
|
||||
|
@ -14,8 +14,3 @@ order by
|
||||
"audit"."entityId" desc,
|
||||
"audit"."entityType" desc,
|
||||
"audit"."createdAt" desc
|
||||
|
||||
-- AuditRepository.removeBefore
|
||||
delete from "audit"
|
||||
where
|
||||
"createdAt" < $1
|
||||
|
@ -1,11 +1,5 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- MemoryRepository.cleanup
|
||||
delete from "memories"
|
||||
where
|
||||
"createdAt" < $1
|
||||
and "isSaved" = $2
|
||||
|
||||
-- MemoryRepository.search
|
||||
select
|
||||
"memories".*,
|
||||
|
@ -16,19 +16,6 @@ where
|
||||
returning
|
||||
*
|
||||
|
||||
-- MoveRepository.cleanMoveHistory
|
||||
delete from "move_history"
|
||||
where
|
||||
"move_history"."entityId" not in (
|
||||
select
|
||||
"id"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."id" = "move_history"."entityId"
|
||||
)
|
||||
and "move_history"."pathType" = 'original'
|
||||
|
||||
-- MoveRepository.cleanMoveHistorySingle
|
||||
delete from "move_history"
|
||||
where
|
||||
|
@ -1,23 +1,5 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- NotificationRepository.cleanup
|
||||
delete from "notifications"
|
||||
where
|
||||
(
|
||||
(
|
||||
"deletedAt" is not null
|
||||
and "deletedAt" < $1
|
||||
)
|
||||
or (
|
||||
"readAt" > $2
|
||||
and "createdAt" < $3
|
||||
)
|
||||
or (
|
||||
"readAt" = $4
|
||||
and "createdAt" < $5
|
||||
)
|
||||
)
|
||||
|
||||
-- NotificationRepository.search
|
||||
select
|
||||
"id",
|
||||
|
@ -100,50 +100,6 @@ where
|
||||
"sharedWithId" = $1
|
||||
and "sharedById" = $2
|
||||
|
||||
-- PartnerRepository.create
|
||||
insert into
|
||||
"partners" ("sharedWithId", "sharedById")
|
||||
values
|
||||
($1, $2)
|
||||
returning
|
||||
*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users" as "sharedBy"
|
||||
where
|
||||
"sharedBy"."id" = "partners"."sharedById"
|
||||
) as obj
|
||||
) as "sharedBy",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
"avatarColor",
|
||||
"profileImagePath",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users" as "sharedWith"
|
||||
where
|
||||
"sharedWith"."id" = "partners"."sharedWithId"
|
||||
) as obj
|
||||
) as "sharedWith"
|
||||
|
||||
-- PartnerRepository.update
|
||||
update "partners"
|
||||
set
|
||||
|
@ -7,34 +7,10 @@ set
|
||||
where
|
||||
"asset_faces"."personId" = $2
|
||||
|
||||
-- PersonRepository.unassignFaces
|
||||
update "asset_faces"
|
||||
set
|
||||
"personId" = $1
|
||||
where
|
||||
"asset_faces"."sourceType" = $2
|
||||
VACUUM
|
||||
ANALYZE asset_faces,
|
||||
face_search,
|
||||
person
|
||||
REINDEX TABLE asset_faces
|
||||
REINDEX TABLE person
|
||||
|
||||
-- PersonRepository.delete
|
||||
delete from "person"
|
||||
where
|
||||
"person"."id" in $1
|
||||
|
||||
-- PersonRepository.deleteFaces
|
||||
delete from "asset_faces"
|
||||
where
|
||||
"asset_faces"."sourceType" = $1
|
||||
VACUUM
|
||||
ANALYZE asset_faces,
|
||||
face_search,
|
||||
person
|
||||
REINDEX TABLE asset_faces
|
||||
REINDEX TABLE person
|
||||
"person"."id" in ($1)
|
||||
|
||||
-- PersonRepository.getAllWithoutFaces
|
||||
select
|
||||
@ -145,18 +121,24 @@ select
|
||||
"asset_faces"."imageHeight" as "oldHeight",
|
||||
"assets"."type",
|
||||
"assets"."originalPath",
|
||||
"asset_files"."path" as "previewPath",
|
||||
"exif"."orientation" as "exifOrientation"
|
||||
"exif"."orientation" as "exifOrientation",
|
||||
(
|
||||
select
|
||||
"asset_files"."path"
|
||||
from
|
||||
"asset_files"
|
||||
where
|
||||
"asset_files"."assetId" = "assets"."id"
|
||||
and "asset_files"."type" = 'preview'
|
||||
) as "previewPath"
|
||||
from
|
||||
"person"
|
||||
inner join "asset_faces" on "asset_faces"."id" = "person"."faceAssetId"
|
||||
inner join "assets" on "asset_faces"."assetId" = "assets"."id"
|
||||
left join "exif" on "exif"."assetId" = "assets"."id"
|
||||
left join "asset_files" on "asset_files"."assetId" = "assets"."id"
|
||||
where
|
||||
"person"."id" = $1
|
||||
and "asset_faces"."deletedAt" is null
|
||||
and "asset_files"."type" = $2
|
||||
|
||||
-- PersonRepository.reassignFace
|
||||
update "asset_faces"
|
||||
@ -222,21 +204,6 @@ where
|
||||
"person"."ownerId" = $3
|
||||
and "asset_faces"."deletedAt" is null
|
||||
|
||||
-- PersonRepository.refreshFaces
|
||||
with
|
||||
"added_embeddings" as (
|
||||
insert into
|
||||
"face_search" ("faceId", "embedding")
|
||||
values
|
||||
($1, $2)
|
||||
)
|
||||
select
|
||||
from
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
|
||||
-- PersonRepository.getFacesByIds
|
||||
select
|
||||
"asset_faces".*,
|
||||
|
@ -1,17 +1,20 @@
|
||||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- SessionRepository.search
|
||||
-- SessionRepository.get
|
||||
select
|
||||
*
|
||||
"id",
|
||||
"expiresAt",
|
||||
"pinExpiresAt"
|
||||
from
|
||||
"sessions"
|
||||
where
|
||||
"sessions"."updatedAt" <= $1
|
||||
"id" = $1
|
||||
|
||||
-- SessionRepository.getByToken
|
||||
select
|
||||
"sessions"."id",
|
||||
"sessions"."updatedAt",
|
||||
"sessions"."pinExpiresAt",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
@ -35,6 +38,10 @@ from
|
||||
"sessions"
|
||||
where
|
||||
"sessions"."token" = $1
|
||||
and (
|
||||
"sessions"."expiresAt" is null
|
||||
or "sessions"."expiresAt" > $2
|
||||
)
|
||||
|
||||
-- SessionRepository.getByUserId
|
||||
select
|
||||
@ -45,6 +52,10 @@ from
|
||||
and "users"."deletedAt" is null
|
||||
where
|
||||
"sessions"."userId" = $1
|
||||
and (
|
||||
"sessions"."expiresAt" is null
|
||||
or "sessions"."expiresAt" > $2
|
||||
)
|
||||
order by
|
||||
"sessions"."updatedAt" desc,
|
||||
"sessions"."createdAt" desc
|
||||
@ -53,3 +64,10 @@ order by
|
||||
delete from "sessions"
|
||||
where
|
||||
"id" = $1::uuid
|
||||
|
||||
-- SessionRepository.lockAll
|
||||
update "sessions"
|
||||
set
|
||||
"pinExpiresAt" = $1
|
||||
where
|
||||
"userId" = $2
|
||||
|
@ -8,15 +8,6 @@ from
|
||||
where
|
||||
"key" = $1
|
||||
|
||||
-- SystemMetadataRepository.set
|
||||
insert into
|
||||
"system_metadata" ("key", "value")
|
||||
values
|
||||
($1, $2)
|
||||
on conflict ("key") do update
|
||||
set
|
||||
"value" = $3
|
||||
|
||||
-- SystemMetadataRepository.delete
|
||||
delete from "system_metadata"
|
||||
where
|
||||
|
@ -58,7 +58,7 @@ from
|
||||
where
|
||||
"userId" = $1
|
||||
order by
|
||||
"value" asc
|
||||
"value"
|
||||
|
||||
-- TagRepository.create
|
||||
insert into
|
||||
@ -94,6 +94,15 @@ where
|
||||
"tagsId" = $1
|
||||
and "assetsId" in ($2)
|
||||
|
||||
-- TagRepository.upsertAssetIds
|
||||
insert into
|
||||
"tag_asset" ("assetId", "tagsIds")
|
||||
values
|
||||
($1, $2)
|
||||
on conflict do nothing
|
||||
returning
|
||||
*
|
||||
|
||||
-- TagRepository.replaceAssetTags
|
||||
begin
|
||||
delete from "tag_asset"
|
||||
@ -107,17 +116,3 @@ on conflict do nothing
|
||||
returning
|
||||
*
|
||||
rollback
|
||||
|
||||
-- TagRepository.deleteEmptyTags
|
||||
begin
|
||||
select
|
||||
"tags"."id",
|
||||
count("assets"."id") as "count"
|
||||
from
|
||||
"assets"
|
||||
inner join "tag_asset" on "tag_asset"."assetsId" = "assets"."id"
|
||||
inner join "tags_closure" on "tags_closure"."id_descendant" = "tag_asset"."tagsId"
|
||||
inner join "tags" on "tags"."id" = "tags_closure"."id_descendant"
|
||||
group by
|
||||
"tags"."id"
|
||||
commit
|
||||
|
@ -15,11 +15,3 @@ from
|
||||
"version_history"
|
||||
order by
|
||||
"createdAt" desc
|
||||
|
||||
-- VersionHistoryRepository.create
|
||||
insert into
|
||||
"version_history" ("version")
|
||||
values
|
||||
($1)
|
||||
returning
|
||||
*
|
||||
|
@ -168,7 +168,7 @@ class AssetAccess {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, assetIds: Set<string>) {
|
||||
async checkOwnerAccess(userId: string, assetIds: Set<string>, hasElevatedPermission: boolean | undefined) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
@ -178,6 +178,7 @@ class AssetAccess {
|
||||
.select('assets.id')
|
||||
.where('assets.id', 'in', [...assetIds])
|
||||
.where('assets.ownerId', '=', userId)
|
||||
.$if(!hasElevatedPermission, (eb) => eb.where('assets.visibility', '!=', AssetVisibility.LOCKED))
|
||||
.execute()
|
||||
.then((assets) => new Set(assets.map((asset) => asset.id)));
|
||||
}
|
||||
@ -305,6 +306,25 @@ class NotificationAccess {
|
||||
}
|
||||
}
|
||||
|
||||
class SessionAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, sessionIds: Set<string>) {
|
||||
if (sessionIds.size === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
.selectFrom('sessions')
|
||||
.select('sessions.id')
|
||||
.where('sessions.id', 'in', [...sessionIds])
|
||||
.where('sessions.userId', '=', userId)
|
||||
.execute()
|
||||
.then((sessions) => new Set(sessions.map((session) => session.id)));
|
||||
}
|
||||
}
|
||||
class StackAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@ -455,6 +475,7 @@ export class AccessRepository {
|
||||
notification: NotificationAccess;
|
||||
person: PersonAccess;
|
||||
partner: PartnerAccess;
|
||||
session: SessionAccess;
|
||||
stack: StackAccess;
|
||||
tag: TagAccess;
|
||||
timeline: TimelineAccess;
|
||||
@ -468,6 +489,7 @@ export class AccessRepository {
|
||||
this.notification = new NotificationAccess(db);
|
||||
this.person = new PersonAccess(db);
|
||||
this.partner = new PartnerAccess(db);
|
||||
this.session = new SessionAccess(db);
|
||||
this.stack = new StackAccess(db);
|
||||
this.tag = new TagAccess(db);
|
||||
this.timeline = new TimelineAccess(db);
|
||||
|
@ -220,8 +220,10 @@ export class AlbumRepository {
|
||||
await this.db.deleteFrom('albums').where('ownerId', '=', userId).execute();
|
||||
}
|
||||
|
||||
async removeAsset(assetId: string): Promise<void> {
|
||||
await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', '=', assetId).execute();
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
@Chunked()
|
||||
async removeAssetsFromAll(assetIds: string[]): Promise<void> {
|
||||
await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', 'in', assetIds).execute();
|
||||
}
|
||||
|
||||
@Chunked({ paramIndex: 1 })
|
||||
|
@ -817,9 +817,7 @@ export class AssetRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
params: [{ libraryId: DummyValue.UUID, importPaths: [DummyValue.STRING], exclusionPatterns: [DummyValue.STRING] }],
|
||||
})
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING], [DummyValue.STRING]] })
|
||||
async detectOfflineExternalAssets(
|
||||
libraryId: string,
|
||||
importPaths: string[],
|
||||
@ -846,9 +844,7 @@ export class AssetRepository {
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({
|
||||
params: [{ libraryId: DummyValue.UUID, paths: [DummyValue.STRING] }],
|
||||
})
|
||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] })
|
||||
async filterNewExternalAssetPaths(libraryId: string, paths: string[]): Promise<string[]> {
|
||||
const result = await this.db
|
||||
.selectFrom(unnest(paths).as('path'))
|
||||
|
@ -38,7 +38,6 @@ export class AuditRepository {
|
||||
return records.map(({ entityId }) => entityId);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.DATE] })
|
||||
async removeBefore(before: Date): Promise<void> {
|
||||
await this.db.deleteFrom('audit').where('createdAt', '<', before).execute();
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ export class CryptoRepository {
|
||||
});
|
||||
}
|
||||
|
||||
newPassword(bytes: number) {
|
||||
randomBytesAsText(bytes: number) {
|
||||
return randomBytes(bytes).toString('base64').replaceAll(/\W/g, '');
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import { IBulkAsset } from 'src/types';
|
||||
export class MemoryRepository implements IBulkAsset {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
cleanup() {
|
||||
return this.db
|
||||
.deleteFrom('memories')
|
||||
|
@ -37,7 +37,6 @@ export class MoveRepository {
|
||||
return this.db.deleteFrom('move_history').where('id', '=', id).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
async cleanMoveHistory(): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('move_history')
|
||||
@ -52,7 +51,7 @@ export class MoveRepository {
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async cleanMoveHistorySingle(assetId: string): Promise<void> {
|
||||
await this.db
|
||||
.deleteFrom('move_history')
|
||||
|
@ -9,7 +9,6 @@ import { NotificationSearchDto } from 'src/dtos/notification.dto';
|
||||
export class NotificationRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
cleanup() {
|
||||
return this.db
|
||||
.deleteFrom('notifications')
|
||||
|
@ -47,7 +47,6 @@ export class PartnerRepository {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] })
|
||||
create(values: Insertable<Partners>) {
|
||||
return this.db
|
||||
.insertInto('partners')
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable } from 'kysely';
|
||||
import { ExpressionBuilder, Insertable, Kysely, Selectable, sql, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
||||
@ -98,18 +98,15 @@ export class PersonRepository {
|
||||
return Number(result.numChangedRows ?? 0);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
|
||||
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
||||
await this.db
|
||||
.updateTable('asset_faces')
|
||||
.set({ personId: null })
|
||||
.where('asset_faces.sourceType', '=', sourceType)
|
||||
.execute();
|
||||
|
||||
await this.vacuum({ reindexVectors: false });
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
async delete(ids: string[]): Promise<void> {
|
||||
if (ids.length === 0) {
|
||||
return;
|
||||
@ -118,11 +115,8 @@ export class PersonRepository {
|
||||
await this.db.deleteFrom('person').where('person.id', 'in', ids).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
|
||||
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
||||
await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute();
|
||||
|
||||
await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
|
||||
}
|
||||
|
||||
getAllFaces(options: GetAllFacesOptions = {}) {
|
||||
@ -265,7 +259,6 @@ export class PersonRepository {
|
||||
.innerJoin('asset_faces', 'asset_faces.id', 'person.faceAssetId')
|
||||
.innerJoin('assets', 'asset_faces.assetId', 'assets.id')
|
||||
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
||||
.leftJoin('asset_files', 'asset_files.assetId', 'assets.id')
|
||||
.select([
|
||||
'person.ownerId',
|
||||
'asset_faces.boundingBoxX1 as x1',
|
||||
@ -276,13 +269,18 @@ export class PersonRepository {
|
||||
'asset_faces.imageHeight as oldHeight',
|
||||
'assets.type',
|
||||
'assets.originalPath',
|
||||
'asset_files.path as previewPath',
|
||||
'exif.orientation as exifOrientation',
|
||||
])
|
||||
.select((eb) =>
|
||||
eb
|
||||
.selectFrom('asset_files')
|
||||
.select('asset_files.path')
|
||||
.whereRef('asset_files.assetId', '=', 'assets.id')
|
||||
.where('asset_files.type', '=', sql.lit(AssetFileType.PREVIEW))
|
||||
.as('previewPath'),
|
||||
)
|
||||
.where('person.id', '=', id)
|
||||
.where('asset_faces.deletedAt', 'is', null)
|
||||
.where('asset_files.type', '=', AssetFileType.PREVIEW)
|
||||
.$narrowType<{ exifImageWidth: NotNull; exifImageHeight: NotNull }>()
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@ -400,7 +398,6 @@ export class PersonRepository {
|
||||
return results.map(({ id }) => id);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[], [], [{ faceId: DummyValue.UUID, embedding: DummyValue.VECTOR }]] })
|
||||
async refreshFaces(
|
||||
facesToAdd: (Insertable<AssetFaces> & { assetId: string })[],
|
||||
faceIdsToRemove: string[],
|
||||
@ -519,7 +516,7 @@ export class PersonRepository {
|
||||
await this.db.updateTable('asset_faces').set({ deletedAt: new Date() }).where('asset_faces.id', '=', id).execute();
|
||||
}
|
||||
|
||||
private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> {
|
||||
async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> {
|
||||
await sql`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db);
|
||||
await sql`REINDEX TABLE asset_faces`.execute(this.db);
|
||||
await sql`REINDEX TABLE person`.execute(this.db);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { DateTime } from 'luxon';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns } from 'src/database';
|
||||
import { DB, Sessions } from 'src/db';
|
||||
@ -13,13 +14,26 @@ export type SessionSearchOptions = { updatedBefore: Date };
|
||||
export class SessionRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] })
|
||||
search(options: SessionSearchOptions) {
|
||||
cleanup() {
|
||||
return this.db
|
||||
.deleteFrom('sessions')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()),
|
||||
eb.and([eb('expiresAt', 'is not', null), eb('expiresAt', '<=', DateTime.now().toJSDate())]),
|
||||
]),
|
||||
)
|
||||
.returning(['id', 'deviceOS', 'deviceType'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
get(id: string) {
|
||||
return this.db
|
||||
.selectFrom('sessions')
|
||||
.selectAll()
|
||||
.where('sessions.updatedAt', '<=', options.updatedBefore)
|
||||
.execute();
|
||||
.select(['id', 'expiresAt', 'pinExpiresAt'])
|
||||
.where('id', '=', id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
@ -37,6 +51,9 @@ export class SessionRepository {
|
||||
).as('user'),
|
||||
])
|
||||
.where('sessions.token', '=', token)
|
||||
.where((eb) =>
|
||||
eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
|
||||
)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@ -47,6 +64,9 @@ export class SessionRepository {
|
||||
.innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null))
|
||||
.selectAll('sessions')
|
||||
.where('sessions.userId', '=', userId)
|
||||
.where((eb) =>
|
||||
eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
|
||||
)
|
||||
.orderBy('sessions.updatedAt', 'desc')
|
||||
.orderBy('sessions.createdAt', 'desc')
|
||||
.execute();
|
||||
@ -69,4 +89,9 @@ export class SessionRepository {
|
||||
async delete(id: string) {
|
||||
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async lockAll(userId: string) {
|
||||
await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute();
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ export class SystemMetadataRepository {
|
||||
return metadata.value as SystemMetadata[T];
|
||||
}
|
||||
|
||||
@GenerateSql({ params: ['metadata_key', { foo: 'bar' }] })
|
||||
async set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void> {
|
||||
await this.db
|
||||
.insertInto('system_metadata')
|
||||
|
@ -68,7 +68,7 @@ export class TagRepository {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getAll(userId: string) {
|
||||
return this.db.selectFrom('tags').select(columns.tag).where('userId', '=', userId).orderBy('value asc').execute();
|
||||
return this.db.selectFrom('tags').select(columns.tag).where('userId', '=', userId).orderBy('value').execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ userId: DummyValue.UUID, color: DummyValue.STRING, value: DummyValue.STRING }] })
|
||||
@ -126,7 +126,7 @@ export class TagRepository {
|
||||
await this.db.deleteFrom('tag_asset').where('tagsId', '=', tagId).where('assetsId', 'in', assetIds).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ assetId: DummyValue.UUID, tagsIds: [DummyValue.UUID] }] })
|
||||
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, tagsIds: [DummyValue.UUID] }]] })
|
||||
@Chunked()
|
||||
upsertAssetIds(items: Insertable<TagAsset>[]) {
|
||||
if (items.length === 0) {
|
||||
@ -160,7 +160,6 @@ export class TagRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
async deleteEmptyTags() {
|
||||
// TODO rewrite as a single statement
|
||||
await this.db.transaction().execute(async (tx) => {
|
||||
|
@ -18,7 +18,6 @@ export class VersionHistoryRepository {
|
||||
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ version: 'v1.123.0' }] })
|
||||
create(version: Insertable<VersionHistory>) {
|
||||
return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TYPE "asset_visibility_enum" ADD VALUE IF NOT EXISTS 'locked';`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// noop
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "sessions" ADD "pinExpiresAt" timestamp with time zone;`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "sessions" DROP COLUMN "pinExpiresAt";`.execute(db);
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "sessions" ADD "expiredAt" timestamp with time zone;`.execute(db);
|
||||
await sql`ALTER TABLE "sessions" ADD "parentId" uuid;`.execute(db);
|
||||
await sql`ALTER TABLE "sessions" ADD CONSTRAINT "FK_afbbabbd7daf5b91de4dca84de8" FOREIGN KEY ("parentId") REFERENCES "sessions" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||
await sql`CREATE INDEX "IDX_afbbabbd7daf5b91de4dca84de" ON "sessions" ("parentId")`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`DROP INDEX "IDX_afbbabbd7daf5b91de4dca84de";`.execute(db);
|
||||
await sql`ALTER TABLE "sessions" DROP CONSTRAINT "FK_afbbabbd7daf5b91de4dca84de8";`.execute(db);
|
||||
await sql`ALTER TABLE "sessions" DROP COLUMN "expiredAt";`.execute(db);
|
||||
await sql`ALTER TABLE "sessions" DROP COLUMN "parentId";`.execute(db);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "sessions" RENAME "expiredAt" TO "expiresAt";`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "sessions" RENAME "expiresAt" TO "expiredAt";`.execute(db);
|
||||
}
|
@ -25,9 +25,15 @@ export class SessionTable {
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||
expiresAt!: Date | null;
|
||||
|
||||
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||
userId!: string;
|
||||
|
||||
@ForeignKeyColumn(() => SessionTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', nullable: true })
|
||||
parentId!: string | null;
|
||||
|
||||
@Column({ default: '' })
|
||||
deviceType!: string;
|
||||
|
||||
@ -36,4 +42,7 @@ export class SessionTable {
|
||||
|
||||
@UpdateIdColumn({ indexName: 'IDX_sessions_update_id' })
|
||||
updateId!: string;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||
pinExpiresAt!: Date | null;
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ describe(AlbumService.name, () => {
|
||||
);
|
||||
|
||||
expect(mocks.user.get).toHaveBeenCalledWith('user-id', {});
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']), false);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
|
||||
id: albumStub.empty.id,
|
||||
userId: 'user-id',
|
||||
@ -207,6 +207,7 @@ describe(AlbumService.name, () => {
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set(['asset-1', 'asset-2']),
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -688,7 +689,11 @@ describe(AlbumService.name, () => {
|
||||
{ success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION },
|
||||
]);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set(['asset-1']),
|
||||
false,
|
||||
);
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
});
|
||||
|
||||
|
@ -18,7 +18,7 @@ describe(ApiKeyService.name, () => {
|
||||
const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.ALL] });
|
||||
const key = 'super-secret';
|
||||
|
||||
mocks.crypto.newPassword.mockReturnValue(key);
|
||||
mocks.crypto.randomBytesAsText.mockReturnValue(key);
|
||||
mocks.apiKey.create.mockResolvedValue(apiKey);
|
||||
|
||||
await sut.create(auth, { name: apiKey.name, permissions: apiKey.permissions });
|
||||
@ -29,7 +29,7 @@ describe(ApiKeyService.name, () => {
|
||||
permissions: apiKey.permissions,
|
||||
userId: apiKey.userId,
|
||||
});
|
||||
expect(mocks.crypto.newPassword).toHaveBeenCalled();
|
||||
expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled();
|
||||
expect(mocks.crypto.hashSha256).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -38,7 +38,7 @@ describe(ApiKeyService.name, () => {
|
||||
const apiKey = factory.apiKey({ userId: auth.user.id });
|
||||
const key = 'super-secret';
|
||||
|
||||
mocks.crypto.newPassword.mockReturnValue(key);
|
||||
mocks.crypto.randomBytesAsText.mockReturnValue(key);
|
||||
mocks.apiKey.create.mockResolvedValue(apiKey);
|
||||
|
||||
await sut.create(auth, { permissions: [Permission.ALL] });
|
||||
@ -49,7 +49,7 @@ describe(ApiKeyService.name, () => {
|
||||
permissions: [Permission.ALL],
|
||||
userId: auth.user.id,
|
||||
});
|
||||
expect(mocks.crypto.newPassword).toHaveBeenCalled();
|
||||
expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled();
|
||||
expect(mocks.crypto.hashSha256).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -9,20 +9,21 @@ import { isGranted } from 'src/utils/access';
|
||||
@Injectable()
|
||||
export class ApiKeyService extends BaseService {
|
||||
async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
|
||||
const secret = this.cryptoRepository.newPassword(32);
|
||||
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
||||
|
||||
if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) {
|
||||
throw new BadRequestException('Cannot grant permissions you do not have');
|
||||
}
|
||||
|
||||
const entity = await this.apiKeyRepository.create({
|
||||
key: this.cryptoRepository.hashSha256(secret),
|
||||
key: tokenHashed,
|
||||
name: dto.name || 'API Key',
|
||||
userId: auth.user.id,
|
||||
permissions: dto.permissions,
|
||||
});
|
||||
|
||||
return { secret, apiKey: this.map(entity) };
|
||||
return { secret: token, apiKey: this.map(entity) };
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise<APIKeyResponseDto> {
|
||||
|
@ -481,7 +481,11 @@ describe(AssetMediaService.name, () => {
|
||||
it('should require the asset.download permission', async () => {
|
||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set(['asset-1']),
|
||||
undefined,
|
||||
);
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||
});
|
||||
@ -512,7 +516,7 @@ describe(AssetMediaService.name, () => {
|
||||
it('should require asset.view permissions', async () => {
|
||||
await expect(sut.viewThumbnail(authStub.admin, 'id', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']), undefined);
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
});
|
||||
@ -611,7 +615,7 @@ describe(AssetMediaService.name, () => {
|
||||
it('should require asset.view permissions', async () => {
|
||||
await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']), undefined);
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
});
|
||||
|
@ -122,6 +122,7 @@ describe(AssetService.name, () => {
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
mapStats,
|
||||
} from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetStatus, JobName, JobStatus, Permission, QueueName } from 'src/enum';
|
||||
import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { ISidecarWriteJob, JobItem, JobOf } from 'src/types';
|
||||
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
||||
@ -125,6 +125,10 @@ export class AssetService extends BaseService {
|
||||
options.rating !== undefined
|
||||
) {
|
||||
await this.assetRepository.updateAll(ids, options);
|
||||
|
||||
if (options.visibility === AssetVisibility.LOCKED) {
|
||||
await this.albumRepository.removeAssetsFromAll(ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,6 +253,7 @@ describe(AuthService.name, () => {
|
||||
id: session.id,
|
||||
updatedAt: session.updatedAt,
|
||||
user: factory.authUser(),
|
||||
pinExpiresAt: null,
|
||||
};
|
||||
|
||||
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
||||
@ -265,7 +266,7 @@ describe(AuthService.name, () => {
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
user: sessionWithToken.user,
|
||||
session: { id: session.id },
|
||||
session: { id: session.id, hasElevatedPermission: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -376,6 +377,7 @@ describe(AuthService.name, () => {
|
||||
id: session.id,
|
||||
updatedAt: session.updatedAt,
|
||||
user: factory.authUser(),
|
||||
pinExpiresAt: null,
|
||||
};
|
||||
|
||||
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
||||
@ -388,7 +390,7 @@ describe(AuthService.name, () => {
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
user: sessionWithToken.user,
|
||||
session: { id: session.id },
|
||||
session: { id: session.id, hasElevatedPermission: false },
|
||||
});
|
||||
});
|
||||
|
||||
@ -398,6 +400,7 @@ describe(AuthService.name, () => {
|
||||
id: session.id,
|
||||
updatedAt: session.updatedAt,
|
||||
user: factory.authUser(),
|
||||
pinExpiresAt: null,
|
||||
};
|
||||
|
||||
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
||||
@ -417,6 +420,7 @@ describe(AuthService.name, () => {
|
||||
id: session.id,
|
||||
updatedAt: session.updatedAt,
|
||||
user: factory.authUser(),
|
||||
pinExpiresAt: null,
|
||||
};
|
||||
|
||||
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
||||
@ -916,13 +920,17 @@ describe(AuthService.name, () => {
|
||||
|
||||
describe('resetPinCode', () => {
|
||||
it('should reset the PIN code', async () => {
|
||||
const currentSession = factory.session();
|
||||
const user = factory.userAdmin();
|
||||
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
||||
mocks.session.lockAll.mockResolvedValue(void 0);
|
||||
mocks.session.update.mockResolvedValue(currentSession);
|
||||
|
||||
await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' });
|
||||
|
||||
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
|
||||
expect(mocks.session.lockAll).toHaveBeenCalledWith(user.id);
|
||||
});
|
||||
|
||||
it('should throw if the PIN code does not match', async () => {
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
PinCodeChangeDto,
|
||||
PinCodeResetDto,
|
||||
PinCodeSetupDto,
|
||||
SessionUnlockDto,
|
||||
SignUpDto,
|
||||
mapLoginResponse,
|
||||
} from 'src/dtos/auth.dto';
|
||||
@ -123,20 +124,21 @@ export class AuthService extends BaseService {
|
||||
|
||||
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
|
||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||
this.resetPinChecks(user, dto);
|
||||
this.validatePinCode(user, dto);
|
||||
|
||||
await this.userRepository.update(auth.user.id, { pinCode: null });
|
||||
await this.sessionRepository.lockAll(auth.user.id);
|
||||
}
|
||||
|
||||
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
|
||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||
this.resetPinChecks(user, dto);
|
||||
this.validatePinCode(user, dto);
|
||||
|
||||
const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
|
||||
await this.userRepository.update(auth.user.id, { pinCode: hashed });
|
||||
}
|
||||
|
||||
private resetPinChecks(
|
||||
private validatePinCode(
|
||||
user: { pinCode: string | null; password: string | null },
|
||||
dto: { pinCode?: string; password?: string },
|
||||
) {
|
||||
@ -444,10 +446,25 @@ export class AuthService extends BaseService {
|
||||
await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
// Pin check
|
||||
let hasElevatedPermission = false;
|
||||
|
||||
if (session.pinExpiresAt) {
|
||||
const pinExpiresAt = DateTime.fromJSDate(session.pinExpiresAt);
|
||||
hasElevatedPermission = pinExpiresAt > now;
|
||||
|
||||
if (hasElevatedPermission && now.plus({ minutes: 5 }) > pinExpiresAt) {
|
||||
await this.sessionRepository.update(session.id, {
|
||||
pinExpiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user: session.user,
|
||||
session: {
|
||||
id: session.id,
|
||||
hasElevatedPermission,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -455,18 +472,39 @@ export class AuthService extends BaseService {
|
||||
throw new UnauthorizedException('Invalid user token');
|
||||
}
|
||||
|
||||
async unlockSession(auth: AuthDto, dto: SessionUnlockDto): Promise<void> {
|
||||
if (!auth.session) {
|
||||
throw new BadRequestException('This endpoint can only be used with a session token');
|
||||
}
|
||||
|
||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||
this.validatePinCode(user, { pinCode: dto.pinCode });
|
||||
|
||||
await this.sessionRepository.update(auth.session.id, {
|
||||
pinExpiresAt: DateTime.now().plus({ minutes: 15 }).toJSDate(),
|
||||
});
|
||||
}
|
||||
|
||||
async lockSession(auth: AuthDto): Promise<void> {
|
||||
if (!auth.session) {
|
||||
throw new BadRequestException('This endpoint can only be used with a session token');
|
||||
}
|
||||
|
||||
await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
|
||||
}
|
||||
|
||||
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
|
||||
const key = this.cryptoRepository.newPassword(32);
|
||||
const token = this.cryptoRepository.hashSha256(key);
|
||||
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
||||
|
||||
await this.sessionRepository.create({
|
||||
token,
|
||||
token: tokenHashed,
|
||||
deviceOS: loginDetails.deviceOS,
|
||||
deviceType: loginDetails.deviceType,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return mapLoginResponse(user, key);
|
||||
return mapLoginResponse(user, token);
|
||||
}
|
||||
|
||||
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
||||
@ -490,9 +528,14 @@ export class AuthService extends BaseService {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const session = auth.session ? await this.sessionRepository.get(auth.session.id) : undefined;
|
||||
|
||||
return {
|
||||
pinCode: !!user.pinCode,
|
||||
password: !!user.password,
|
||||
isElevated: !!auth.session?.hasElevatedPermission,
|
||||
expiresAt: session?.expiresAt?.toISOString(),
|
||||
pinExpiresAt: session?.pinExpiresAt?.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export class CliService extends BaseService {
|
||||
}
|
||||
|
||||
const providedPassword = await ask(mapUserAdmin(admin));
|
||||
const password = providedPassword || this.cryptoRepository.newPassword(24);
|
||||
const password = providedPassword || this.cryptoRepository.randomBytesAsText(24);
|
||||
const hashedPassword = await this.cryptoRepository.hashBcrypt(password, SALT_ROUNDS);
|
||||
|
||||
await this.userRepository.update(admin.id, { password: hashedPassword });
|
||||
|
@ -1310,7 +1310,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
|
||||
);
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
expect(mocks.album.removeAssetsFromAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle not finding a match', async () => {
|
||||
@ -1331,7 +1331,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
|
||||
);
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
expect(mocks.album.removeAssetsFromAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should link photo and video', async () => {
|
||||
@ -1356,7 +1356,7 @@ describe(MetadataService.name, () => {
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
});
|
||||
expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
||||
expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
|
||||
});
|
||||
|
||||
it('should notify clients on live photo link', async () => {
|
||||
|
@ -158,7 +158,7 @@ export class MetadataService extends BaseService {
|
||||
await Promise.all([
|
||||
this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }),
|
||||
this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.HIDDEN }),
|
||||
this.albumRepository.removeAsset(motionAsset.id),
|
||||
this.albumRepository.removeAssetsFromAll([motionAsset.id]),
|
||||
]);
|
||||
|
||||
await this.eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
|
||||
|
@ -459,6 +459,7 @@ describe(PersonService.name, () => {
|
||||
await sut.handleQueueDetectFaces({ force: false });
|
||||
|
||||
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(false);
|
||||
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.FACE_DETECTION,
|
||||
@ -475,6 +476,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]);
|
||||
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true });
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath);
|
||||
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
@ -492,6 +494,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
expect(mocks.person.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.person.deleteFaces).not.toHaveBeenCalled();
|
||||
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.unlink).not.toHaveBeenCalled();
|
||||
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(undefined);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
@ -521,6 +524,7 @@ describe(PersonService.name, () => {
|
||||
]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
||||
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true });
|
||||
});
|
||||
});
|
||||
|
||||
@ -584,6 +588,7 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
|
||||
lastRun: expect.any(String),
|
||||
});
|
||||
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should queue all assets', async () => {
|
||||
@ -611,6 +616,7 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
|
||||
lastRun: expect.any(String),
|
||||
});
|
||||
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false });
|
||||
});
|
||||
|
||||
it('should run nightly if new face has been added since last run', async () => {
|
||||
@ -629,11 +635,14 @@ describe(PersonService.name, () => {
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([]);
|
||||
mocks.person.unassignFaces.mockResolvedValue();
|
||||
|
||||
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
|
||||
await sut.handleQueueRecognizeFaces({ force: false, nightly: true });
|
||||
|
||||
expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
|
||||
expect(mocks.person.getLatestFaceDate).toHaveBeenCalledOnce();
|
||||
expect(mocks.person.getAllFaces).toHaveBeenCalledWith(undefined);
|
||||
expect(mocks.person.getAllFaces).toHaveBeenCalledWith({
|
||||
personId: null,
|
||||
sourceType: SourceType.MACHINE_LEARNING,
|
||||
});
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.FACIAL_RECOGNITION,
|
||||
@ -643,6 +652,7 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
|
||||
lastRun: expect.any(String),
|
||||
});
|
||||
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip nightly if no new face has been added since last run', async () => {
|
||||
@ -660,6 +670,7 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.person.getAllFaces).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
||||
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete existing people if forced', async () => {
|
||||
@ -688,6 +699,7 @@ describe(PersonService.name, () => {
|
||||
]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
||||
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -259,6 +259,7 @@ export class PersonService extends BaseService {
|
||||
if (force) {
|
||||
await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
||||
await this.handlePersonCleanup();
|
||||
await this.personRepository.vacuum({ reindexVectors: true });
|
||||
}
|
||||
|
||||
let jobs: JobItem[] = [];
|
||||
@ -409,6 +410,7 @@ export class PersonService extends BaseService {
|
||||
if (force) {
|
||||
await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
||||
await this.handlePersonCleanup();
|
||||
await this.personRepository.vacuum({ reindexVectors: false });
|
||||
} else if (waiting) {
|
||||
this.logger.debug(
|
||||
`Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`,
|
||||
|
@ -17,29 +17,9 @@ describe('SessionService', () => {
|
||||
});
|
||||
|
||||
describe('handleCleanup', () => {
|
||||
it('should return skipped if nothing is to be deleted', async () => {
|
||||
mocks.session.search.mockResolvedValue([]);
|
||||
await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SKIPPED);
|
||||
expect(mocks.session.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete sessions', async () => {
|
||||
mocks.session.search.mockResolvedValue([
|
||||
{
|
||||
createdAt: new Date('1970-01-01T00:00:00.00Z'),
|
||||
updatedAt: new Date('1970-01-02T00:00:00.00Z'),
|
||||
deviceOS: '',
|
||||
deviceType: '',
|
||||
id: '123',
|
||||
token: '420',
|
||||
userId: '42',
|
||||
updateId: 'uuid-v7',
|
||||
},
|
||||
]);
|
||||
mocks.session.delete.mockResolvedValue();
|
||||
|
||||
it('should clean sessions', async () => {
|
||||
mocks.session.cleanup.mockResolvedValue([]);
|
||||
await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SUCCESS);
|
||||
expect(mocks.session.delete).toHaveBeenCalledWith('123');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { DateTime } from 'luxon';
|
||||
import { OnJob } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
||||
import { SessionCreateDto, SessionCreateResponseDto, SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
||||
import { JobName, JobStatus, Permission, QueueName } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
||||
@ -10,16 +10,8 @@ import { BaseService } from 'src/services/base.service';
|
||||
export class SessionService extends BaseService {
|
||||
@OnJob({ name: JobName.CLEAN_OLD_SESSION_TOKENS, queue: QueueName.BACKGROUND_TASK })
|
||||
async handleCleanup(): Promise<JobStatus> {
|
||||
const sessions = await this.sessionRepository.search({
|
||||
updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(),
|
||||
});
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return JobStatus.SKIPPED;
|
||||
}
|
||||
|
||||
const sessions = await this.sessionRepository.cleanup();
|
||||
for (const session of sessions) {
|
||||
await this.sessionRepository.delete(session.id);
|
||||
this.logger.verbose(`Deleted expired session token: ${session.deviceOS}/${session.deviceType}`);
|
||||
}
|
||||
|
||||
@ -28,6 +20,25 @@ export class SessionService extends BaseService {
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: SessionCreateDto): Promise<SessionCreateResponseDto> {
|
||||
if (!auth.session) {
|
||||
throw new BadRequestException('This endpoint can only be used with a session token');
|
||||
}
|
||||
|
||||
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
||||
const session = await this.sessionRepository.create({
|
||||
parentId: auth.session.id,
|
||||
userId: auth.user.id,
|
||||
expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
|
||||
deviceType: dto.deviceType,
|
||||
deviceOS: dto.deviceOS,
|
||||
token: tokenHashed,
|
||||
});
|
||||
|
||||
return { ...mapSession(session), token };
|
||||
}
|
||||
|
||||
async getAll(auth: AuthDto): Promise<SessionResponseDto[]> {
|
||||
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
||||
return sessions.map((session) => mapSession(session, auth.session?.id));
|
||||
@ -38,6 +49,11 @@ export class SessionService extends BaseService {
|
||||
await this.sessionRepository.delete(id);
|
||||
}
|
||||
|
||||
async lock(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.SESSION_LOCK, ids: [id] });
|
||||
await this.sessionRepository.update(id, { pinExpiresAt: null });
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto): Promise<void> {
|
||||
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
||||
for (const session of sessions) {
|
||||
|
@ -156,6 +156,7 @@ describe(SharedLinkService.name, () => {
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
false,
|
||||
);
|
||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||
type: SharedLinkType.INDIVIDUAL,
|
||||
@ -186,6 +187,7 @@ describe(SharedLinkService.name, () => {
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set([assetStub.image.id]),
|
||||
false,
|
||||
);
|
||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||
type: SharedLinkType.INDIVIDUAL,
|
||||
|
@ -81,7 +81,7 @@ const checkSharedLinkAccess = async (
|
||||
|
||||
case Permission.ASSET_SHARE: {
|
||||
// TODO: fix this to not use sharedLink.userId for access control
|
||||
return await access.asset.checkOwnerAccess(sharedLink.userId, ids);
|
||||
return await access.asset.checkOwnerAccess(sharedLink.userId, ids, false);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_READ: {
|
||||
@ -119,38 +119,38 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
}
|
||||
|
||||
case Permission.ASSET_READ: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_SHARE: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, false);
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_VIEW: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DOWNLOAD: {
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPDATE: {
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DELETE: {
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_READ: {
|
||||
@ -280,6 +280,13 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||
return await access.partner.checkUpdateAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.SESSION_READ:
|
||||
case Permission.SESSION_UPDATE:
|
||||
case Permission.SESSION_DELETE:
|
||||
case Permission.SESSION_LOCK: {
|
||||
return access.session.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.STACK_READ: {
|
||||
return access.stack.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
6
server/test/fixtures/auth.stub.ts
vendored
6
server/test/fixtures/auth.stub.ts
vendored
@ -1,4 +1,4 @@
|
||||
import { Session } from 'src/database';
|
||||
import { AuthSession } from 'src/database';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
|
||||
const authUser = {
|
||||
@ -26,7 +26,7 @@ export const authStub = {
|
||||
user: authUser.user1,
|
||||
session: {
|
||||
id: 'token-id',
|
||||
} as Session,
|
||||
} as AuthSession,
|
||||
}),
|
||||
user2: Object.freeze<AuthDto>({
|
||||
user: {
|
||||
@ -39,7 +39,7 @@ export const authStub = {
|
||||
},
|
||||
session: {
|
||||
id: 'token-id',
|
||||
} as Session,
|
||||
} as AuthSession,
|
||||
}),
|
||||
adminSharedLink: Object.freeze({
|
||||
user: authUser.admin,
|
||||
|
1
server/test/fixtures/shared-link.stub.ts
vendored
1
server/test/fixtures/shared-link.stub.ts
vendored
@ -70,6 +70,7 @@ const assetResponse: AssetResponseDto = {
|
||||
isTrashed: false,
|
||||
libraryId: 'library-id',
|
||||
hasMetadata: true,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
};
|
||||
|
||||
const assetResponseWithoutMetadata = {
|
||||
|
@ -50,6 +50,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||
checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
session: {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
stack: {
|
||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
@ -12,6 +12,6 @@ export const newCryptoRepositoryMock = (): Mocked<RepositoryInterface<CryptoRepo
|
||||
verifySha256: vitest.fn().mockImplementation(() => true),
|
||||
hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)),
|
||||
hashFile: vitest.fn().mockImplementation((input) => `${input} (file-hashed)`),
|
||||
newPassword: vitest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')),
|
||||
randomBytesAsText: vitest.fn().mockReturnValue(Buffer.from('random-bytes').toString('base64')),
|
||||
};
|
||||
};
|
||||
|
@ -33,5 +33,6 @@ export const newPersonRepositoryMock = (): Mocked<RepositoryInterface<PersonRepo
|
||||
createAssetFace: vitest.fn(),
|
||||
deleteAssetFace: vitest.fn(),
|
||||
softDeleteAssetFaces: vitest.fn(),
|
||||
vacuum: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
@ -58,7 +58,7 @@ const authFactory = ({
|
||||
}
|
||||
|
||||
if (session) {
|
||||
auth.session = { id: session.id };
|
||||
auth.session = { id: session.id, hasElevatedPermission: false };
|
||||
}
|
||||
|
||||
if (sharedLink) {
|
||||
@ -126,7 +126,10 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
|
||||
deviceOS: 'android',
|
||||
deviceType: 'mobile',
|
||||
token: 'abc123',
|
||||
parentId: null,
|
||||
expiresAt: null,
|
||||
userId: newUuid(),
|
||||
pinExpiresAt: newDate(),
|
||||
...session,
|
||||
});
|
||||
|
||||
|
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.20.0",
|
||||
"@immich/ui": "^0.21.1",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.11.5",
|
||||
@ -1337,9 +1337,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@immich/ui": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.20.0.tgz",
|
||||
"integrity": "sha512-euK3N0AhQLB28qFteorRKyDUdet3UpA9MEAd8eBLbTtTFZKvZismBGa4J7pHbQrSkuOlbmJD5LJuM575q8zigQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.21.1.tgz",
|
||||
"integrity": "sha512-ofDbLMYgM3Bnrv1nCbyPV5Gw9PdWvyhTAJPtojw4C3r2m7CbRW1kJDHt5M79n6xAVgjMOFyre1lOE5cwSSvRQA==",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@mdi/js": "^7.4.47",
|
||||
|
@ -18,7 +18,8 @@
|
||||
"lint:p": "eslint-p . --max-warnings 0 --concurrency=4",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"format:fix": "prettier --write . && npm run format:i18n",
|
||||
"format:i18n": "npx --yes sort-json ../i18n/*.json",
|
||||
"test": "vitest --run",
|
||||
"test:cov": "vitest --coverage",
|
||||
"test:watch": "vitest dev",
|
||||
@ -27,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.20.0",
|
||||
"@immich/ui": "^0.21.1",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.11.5",
|
||||
|
@ -21,34 +21,6 @@
|
||||
--immich-dark-success: 56 142 60;
|
||||
--immich-dark-warning: 245 124 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* light */
|
||||
--immich-ui-primary: 66 80 175;
|
||||
--immich-ui-dark: 58 58 58;
|
||||
--immich-ui-light: 255 255 255;
|
||||
--immich-ui-success: 16 188 99;
|
||||
--immich-ui-danger: 200 60 60;
|
||||
--immich-ui-warning: 216 143 64;
|
||||
--immich-ui-info: 8 111 230;
|
||||
--immich-ui-gray: 246 246 246;
|
||||
|
||||
--immich-ui-default-border: 209 213 219;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* dark */
|
||||
--immich-ui-primary: 172 203 250;
|
||||
--immich-ui-light: 0 0 0;
|
||||
--immich-ui-dark: 229 231 235;
|
||||
--immich-ui-danger: 246 125 125;
|
||||
--immich-ui-success: 72 237 152;
|
||||
--immich-ui-warning: 254 197 132;
|
||||
--immich-ui-info: 121 183 254;
|
||||
--immich-ui-gray: 33 33 33;
|
||||
|
||||
--immich-ui-default-border: 55 65 81;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -13,6 +13,8 @@ type ActionMap = {
|
||||
[AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
|
||||
[AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
|
||||
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
|
||||
[AssetAction.SET_VISIBILITY_LOCKED]: { asset: AssetResponseDto };
|
||||
[AssetAction.SET_VISIBILITY_TIMELINE]: { asset: AssetResponseDto };
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
|
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetVisibility, updateAssets, Visibility, type AssetResponseDto } from '@immich/sdk';
|
||||
import { mdiEyeOffOutline, mdiFolderMoveOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { OnAction, PreAction } from './action';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
onAction: OnAction;
|
||||
preAction: PreAction;
|
||||
}
|
||||
|
||||
let { asset, onAction, preAction }: Props = $props();
|
||||
const isLocked = asset.visibility === Visibility.Locked;
|
||||
|
||||
const toggleLockedVisibility = async () => {
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
title: isLocked ? $t('remove_from_locked_folder') : $t('move_to_locked_folder'),
|
||||
prompt: isLocked ? $t('remove_from_locked_folder_confirmation') : $t('move_to_locked_folder_confirmation'),
|
||||
confirmText: $t('move'),
|
||||
confirmColor: isLocked ? 'danger' : 'primary',
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
preAction({
|
||||
type: isLocked ? AssetAction.SET_VISIBILITY_TIMELINE : AssetAction.SET_VISIBILITY_LOCKED,
|
||||
asset,
|
||||
});
|
||||
|
||||
await updateAssets({
|
||||
assetBulkUpdateDto: {
|
||||
ids: [asset.id],
|
||||
visibility: isLocked ? AssetVisibility.Timeline : AssetVisibility.Locked,
|
||||
},
|
||||
});
|
||||
|
||||
onAction({
|
||||
type: isLocked ? AssetAction.SET_VISIBILITY_TIMELINE : AssetAction.SET_VISIBILITY_LOCKED,
|
||||
asset,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_save_settings'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<MenuOption
|
||||
onClick={() => toggleLockedVisibility()}
|
||||
text={isLocked ? $t('move_off_locked_folder') : $t('add_to_locked_folder')}
|
||||
icon={isLocked ? mdiFolderMoveOutline : mdiEyeOffOutline}
|
||||
/>
|
@ -12,6 +12,7 @@
|
||||
import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte';
|
||||
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte';
|
||||
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte';
|
||||
import SetVisibilityAction from '$lib/components/asset-viewer/actions/set-visibility-action.svelte';
|
||||
import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
|
||||
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
|
||||
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
|
||||
@ -27,6 +28,7 @@
|
||||
import {
|
||||
AssetJobName,
|
||||
AssetTypeEnum,
|
||||
Visibility,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
type PersonResponseDto,
|
||||
@ -91,6 +93,7 @@
|
||||
const sharedLink = getSharedLink();
|
||||
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
||||
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
||||
let isLocked = $derived(asset.visibility === Visibility.Locked);
|
||||
|
||||
// $: showEditorButton =
|
||||
// isOwner &&
|
||||
@ -112,7 +115,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2 overflow-x-auto text-white" data-testid="asset-viewer-navbar-actions">
|
||||
{#if !asset.isTrashed && $user}
|
||||
{#if !asset.isTrashed && $user && !isLocked}
|
||||
<ShareAction {asset} />
|
||||
{/if}
|
||||
{#if asset.isOffline}
|
||||
@ -159,18 +162,21 @@
|
||||
<DeleteAction {asset} {onAction} {preAction} />
|
||||
|
||||
<ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}>
|
||||
{#if showSlideshow}
|
||||
{#if showSlideshow && !isLocked}
|
||||
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
||||
{/if}
|
||||
{#if showDownloadButton}
|
||||
<DownloadAction {asset} menuItem />
|
||||
{/if}
|
||||
|
||||
{#if !isLocked}
|
||||
{#if asset.isTrashed}
|
||||
<RestoreAction {asset} {onAction} />
|
||||
{:else}
|
||||
<AddToAlbumAction {asset} {onAction} />
|
||||
<AddToAlbumAction {asset} {onAction} shared />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isOwner}
|
||||
{#if stack}
|
||||
@ -183,9 +189,11 @@
|
||||
{#if person}
|
||||
<SetFeaturedPhotoAction {asset} {person} />
|
||||
{/if}
|
||||
{#if asset.type === AssetTypeEnum.Image}
|
||||
{#if asset.type === AssetTypeEnum.Image && !isLocked}
|
||||
<SetProfilePictureAction {asset} />
|
||||
{/if}
|
||||
|
||||
{#if !isLocked}
|
||||
<ArchiveAction {asset} {onAction} {preAction} />
|
||||
<MenuOption
|
||||
icon={mdiUpload}
|
||||
@ -199,6 +207,11 @@
|
||||
text={$t('view_in_timeline')}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !asset.isTrashed}
|
||||
<SetVisibilityAction {asset} {onAction} {preAction} />
|
||||
{/if}
|
||||
<hr />
|
||||
<MenuOption
|
||||
icon={mdiHeadSyncOutline}
|
||||
|
@ -1,20 +0,0 @@
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
|
||||
describe('Button component', () => {
|
||||
it('should render as a button', () => {
|
||||
render(Button);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAttribute('type', 'button');
|
||||
expect(button).not.toHaveAttribute('href');
|
||||
});
|
||||
|
||||
it('should render as a link if href prop is set', () => {
|
||||
render(Button, { props: { href: '/test' } });
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute('href', '/test');
|
||||
expect(link).not.toHaveAttribute('type');
|
||||
});
|
||||
});
|
@ -1,123 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
export type Color =
|
||||
| 'primary'
|
||||
| 'primary-inversed'
|
||||
| 'secondary'
|
||||
| 'transparent-primary'
|
||||
| 'text-primary'
|
||||
| 'light-red'
|
||||
| 'red'
|
||||
| 'green'
|
||||
| 'gray'
|
||||
| 'transparent-gray'
|
||||
| 'dark-gray'
|
||||
| 'overlay-primary';
|
||||
export type Size = 'tiny' | 'icon' | 'link' | 'sm' | 'base' | 'lg';
|
||||
export type Rounded = 'lg' | '3xl' | 'full' | 'none';
|
||||
export type Shadow = 'md' | false;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
type?: string;
|
||||
href?: string;
|
||||
color?: Color;
|
||||
size?: Size;
|
||||
rounded?: Rounded;
|
||||
shadow?: Shadow;
|
||||
fullwidth?: boolean;
|
||||
border?: boolean;
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
onclick?: (event: MouseEvent) => void;
|
||||
onfocus?: () => void;
|
||||
onblur?: () => void;
|
||||
form?: string;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | undefined | null;
|
||||
}
|
||||
|
||||
let {
|
||||
type = 'button',
|
||||
href = undefined,
|
||||
color = 'primary',
|
||||
size = 'base',
|
||||
rounded = '3xl',
|
||||
shadow = 'md',
|
||||
fullwidth = false,
|
||||
border = false,
|
||||
class: className = '',
|
||||
children,
|
||||
onclick,
|
||||
onfocus,
|
||||
onblur,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
const colorClasses: Record<Color, string> = {
|
||||
primary:
|
||||
'bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80 hover:bg-immich-primary/90',
|
||||
secondary:
|
||||
'bg-gray-500 dark:bg-gray-200 text-white dark:text-immich-dark-gray hover:bg-gray-500/90 dark:hover:bg-gray-200/90',
|
||||
'transparent-primary': 'text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700',
|
||||
'text-primary':
|
||||
'text-immich-primary dark:text-immich-dark-primary dark:hover:bg-immich-dark-primary/10 hover:bg-immich-primary/10',
|
||||
'light-red': 'bg-[#F9DEDC] text-[#410E0B] hover:bg-red-50',
|
||||
red: 'bg-red-500 text-white hover:bg-red-400',
|
||||
green: 'bg-green-400 text-gray-800 hover:bg-green-400/90',
|
||||
gray: 'bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 text-white dark:text-immich-dark-gray',
|
||||
'transparent-gray':
|
||||
'dark:text-immich-dark-fg hover:bg-immich-primary/5 hover:text-gray-700 hover:dark:text-immich-dark-fg dark:hover:bg-immich-dark-primary/25',
|
||||
'dark-gray':
|
||||
'dark:border-immich-dark-gray dark:bg-gray-500 dark:hover:bg-immich-dark-primary/50 hover:bg-immich-primary/10 dark:text-white',
|
||||
'overlay-primary': 'text-gray-500 hover:bg-gray-100',
|
||||
'primary-inversed':
|
||||
'bg-immich-dark-primary dark:bg-immich-primary text-black dark:text-white hover:bg-immich-dark-primary/80 dark:hover:bg-immich-primary/90',
|
||||
};
|
||||
|
||||
const sizeClasses: Record<Size, string> = {
|
||||
tiny: 'p-0 ms-2 me-0 align-top',
|
||||
icon: 'p-2.5',
|
||||
link: 'p-2 font-medium',
|
||||
sm: 'px-4 py-2 text-sm font-medium',
|
||||
base: 'px-6 py-3 font-medium',
|
||||
lg: 'px-6 py-4 font-semibold',
|
||||
};
|
||||
|
||||
const roundedClasses: Record<Rounded, string> = {
|
||||
none: '',
|
||||
lg: 'rounded-lg',
|
||||
'3xl': 'rounded-3xl',
|
||||
full: 'rounded-full',
|
||||
};
|
||||
|
||||
let computedClass = $derived(
|
||||
[
|
||||
className,
|
||||
colorClasses[color],
|
||||
sizeClasses[size],
|
||||
roundedClasses[rounded],
|
||||
shadow === 'md' && 'shadow-md',
|
||||
fullwidth && 'w-full',
|
||||
border && 'border',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? 'a' : 'button'}
|
||||
type={href ? undefined : type}
|
||||
{href}
|
||||
{onclick}
|
||||
{onfocus}
|
||||
{onblur}
|
||||
class="inline-flex items-center justify-center transition-colors disabled:cursor-not-allowed disabled:opacity-60 disabled:pointer-events-none {computedClass}"
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getTabbable } from '$lib/utils/focus-util';
|
||||
import { Button } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import Button from './button.svelte';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
@ -58,8 +58,7 @@
|
||||
|
||||
<div class="absolute top-2 start-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
|
||||
<Button
|
||||
size="sm"
|
||||
rounded="none"
|
||||
size="small"
|
||||
onclick={moveFocus}
|
||||
class={getBreakpoint()}
|
||||
onfocus={() => (isFocused = true)}
|
||||
|
@ -74,7 +74,7 @@
|
||||
|
||||
<section
|
||||
transition:fly={{ x: 360, duration: 100, easing: linear }}
|
||||
class="absolute top-0 h-full w-[360px] overflow-x-hidden p-2 dark:text-immich-dark-fg"
|
||||
class="absolute top-0 h-full w-[360px] overflow-x-hidden p-2 dark:text-immich-dark-fg bg-light"
|
||||
>
|
||||
<div class="flex place-items-center justify-between gap-2">
|
||||
{#if !searchFaces}
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
@ -44,6 +44,6 @@
|
||||
inputClass="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
||||
bind:showLoadingSpinner={isSearchingPeople}
|
||||
/>
|
||||
<Button size="sm" type="submit">{$t('done')}</Button>
|
||||
<Button size="small" shape="round" type="submit">{$t('done')}</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
@ -13,6 +11,7 @@
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updatePeople, type PersonResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
@ -126,11 +125,7 @@
|
||||
<CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} onclick={handleResetVisibility} />
|
||||
<CircleIconButton title={toggleButton.label} icon={toggleButton.icon} onclick={handleToggleVisibility} />
|
||||
</div>
|
||||
{#if !showLoadingSpinner}
|
||||
<Button onclick={handleSaveVisibility} size="sm" rounded="lg">{$t('done')}</Button>
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
<Button loading={showLoadingSpinner} onclick={handleSaveVisibility} size="small">{$t('done')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -6,13 +6,13 @@
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
@ -108,10 +108,9 @@
|
||||
<div></div>
|
||||
{/snippet}
|
||||
{#snippet trailing()}
|
||||
<Button size="sm" disabled={!hasSelection} onclick={handleMerge}>
|
||||
<Icon path={mdiMerge} size={18} />
|
||||
<span class="ms-2">{$t('merge')}</span></Button
|
||||
>
|
||||
<Button leadingIcon={mdiMerge} size="small" shape="round" disabled={!hasSelection} onclick={handleMerge}>
|
||||
{$t('merge')}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
<section class="px-[70px] pt-[100px]">
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
@ -9,14 +8,13 @@
|
||||
type AssetFaceUpdateItem,
|
||||
type PersonResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { mdiMerge, mdiPlus } from '@mdi/js';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import FaceThumbnail from './face-thumbnail.svelte';
|
||||
import PeopleList from './people-list.svelte';
|
||||
@ -130,33 +128,27 @@
|
||||
{#snippet trailing()}
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
shape="round"
|
||||
title={$t('create_new_person_hint')}
|
||||
size="sm"
|
||||
leadingIcon={mdiPlus}
|
||||
loading={showLoadingSpinnerCreate}
|
||||
size="small"
|
||||
disabled={disableButtons || hasSelection}
|
||||
onclick={handleCreate}
|
||||
>
|
||||
{#if !showLoadingSpinnerCreate}
|
||||
<Icon path={mdiPlus} size={18} />
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
<span class="ms-2"> {$t('create_new_person')}</span></Button
|
||||
{$t('create_new_person')}</Button
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
size="small"
|
||||
shape="round"
|
||||
title={$t('reassing_hint')}
|
||||
leadingIcon={mdiMerge}
|
||||
loading={showLoadingSpinnerReassign}
|
||||
disabled={disableButtons || !hasSelection}
|
||||
onclick={handleReassign}
|
||||
>
|
||||
{#if !showLoadingSpinnerReassign}
|
||||
<div>
|
||||
<Icon path={mdiMerge} size={18} class="rotate-180" />
|
||||
</div>
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
<span class="ms-2"> {$t('reassign')}</span></Button
|
||||
>
|
||||
{$t('reassign')}
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user