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:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
outputs:
|
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_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_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' }}
|
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
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
|
i18n:
|
||||||
|
- 'i18n/**'
|
||||||
web:
|
web:
|
||||||
- 'web/**'
|
- 'web/**'
|
||||||
- 'i18n/**'
|
- 'i18n/**'
|
||||||
@ -262,6 +265,46 @@ jobs:
|
|||||||
run: npm run test:cov
|
run: npm run test:cov
|
||||||
if: ${{ !cancelled() }}
|
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:
|
e2e-tests-lint:
|
||||||
name: End-to-End Lint
|
name: End-to-End Lint
|
||||||
needs: pre-job
|
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
|
[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
|
[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-lp]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#low-power-encoding
|
||||||
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
|
[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
|
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
|
||||||
|
@ -202,7 +202,6 @@ describe('/asset', () => {
|
|||||||
{
|
{
|
||||||
name: 'Marie Curie',
|
name: 'Marie Curie',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '',
|
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
faces: [
|
faces: [
|
||||||
{
|
{
|
||||||
@ -219,7 +218,6 @@ describe('/asset', () => {
|
|||||||
{
|
{
|
||||||
name: 'Pierre Curie',
|
name: 'Pierre Curie',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
thumbnailPath: '',
|
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
faces: [
|
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",
|
"about": "About",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"account_settings": "Account Settings",
|
"account_settings": "Account Settings",
|
||||||
@ -39,6 +26,7 @@
|
|||||||
"add_to_album": "Add to album",
|
"add_to_album": "Add to album",
|
||||||
"add_to_album_bottom_sheet_added": "Added to {album}",
|
"add_to_album_bottom_sheet_added": "Added to {album}",
|
||||||
"add_to_album_bottom_sheet_already_exists": "Already in {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_to_shared_album": "Add to shared album",
|
||||||
"add_url": "Add URL",
|
"add_url": "Add URL",
|
||||||
"added_to_archive": "Added to archive",
|
"added_to_archive": "Added to archive",
|
||||||
@ -625,6 +613,7 @@
|
|||||||
"change_password_form_new_password": "New Password",
|
"change_password_form_new_password": "New Password",
|
||||||
"change_password_form_password_mismatch": "Passwords do not match",
|
"change_password_form_password_mismatch": "Passwords do not match",
|
||||||
"change_password_form_reenter_new_password": "Re-enter New Password",
|
"change_password_form_reenter_new_password": "Re-enter New Password",
|
||||||
|
"change_pin_code": "Change PIN code",
|
||||||
"change_your_password": "Change your password",
|
"change_your_password": "Change your password",
|
||||||
"changed_visibility_successfully": "Changed visibility successfully",
|
"changed_visibility_successfully": "Changed visibility successfully",
|
||||||
"check_all": "Check All",
|
"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_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_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_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",
|
"confirm_password": "Confirm password",
|
||||||
"contain": "Contain",
|
"contain": "Contain",
|
||||||
"context": "Context",
|
"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_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.",
|
||||||
"create_user": "Create user",
|
"create_user": "Create user",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
|
"created_at": "Created",
|
||||||
"crop": "Crop",
|
"crop": "Crop",
|
||||||
"curated_object_page_title": "Things",
|
"curated_object_page_title": "Things",
|
||||||
"current_device": "Current device",
|
"current_device": "Current device",
|
||||||
|
"current_pin_code": "Current PIN code",
|
||||||
"current_server_address": "Current server address",
|
"current_server_address": "Current server address",
|
||||||
"custom_locale": "Custom Locale",
|
"custom_locale": "Custom Locale",
|
||||||
"custom_locale_description": "Format dates and numbers based on the language and the region",
|
"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_aspect_ratios": "Aspect ratios",
|
||||||
"editor_crop_tool_h2_rotation": "Rotation",
|
"editor_crop_tool_h2_rotation": "Rotation",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"email_notifications": "Email notifications",
|
||||||
"empty_folder": "This folder is empty",
|
"empty_folder": "This folder is empty",
|
||||||
"empty_trash": "Empty trash",
|
"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!",
|
"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",
|
"end_date": "End date",
|
||||||
"enqueued": "Enqueued",
|
"enqueued": "Enqueued",
|
||||||
"enter_wifi_name": "Enter Wi-Fi name",
|
"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": "Error",
|
||||||
"error_change_sort_album": "Failed to change album sort order",
|
"error_change_sort_album": "Failed to change album sort order",
|
||||||
"error_delete_face": "Error deleting face from asset",
|
"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_all_devices": "Unable to log out all devices",
|
||||||
"unable_to_log_out_device": "Unable to log out device",
|
"unable_to_log_out_device": "Unable to log out device",
|
||||||
"unable_to_login_with_oauth": "Unable to login with OAuth",
|
"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_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_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",
|
"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",
|
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"hour": "Hour",
|
"hour": "Hour",
|
||||||
|
"id": "ID",
|
||||||
"ignore_icloud_photos": "Ignore iCloud photos",
|
"ignore_icloud_photos": "Ignore iCloud photos",
|
||||||
"ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
|
"ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
@ -1145,6 +1142,8 @@
|
|||||||
"location_picker_latitude_hint": "Enter your latitude here",
|
"location_picker_latitude_hint": "Enter your latitude here",
|
||||||
"location_picker_longitude_error": "Enter a valid longitude",
|
"location_picker_longitude_error": "Enter a valid longitude",
|
||||||
"location_picker_longitude_hint": "Enter your longitude here",
|
"location_picker_longitude_hint": "Enter your longitude here",
|
||||||
|
"lock": "Lock",
|
||||||
|
"locked_folder": "Locked Folder",
|
||||||
"log_out": "Log out",
|
"log_out": "Log out",
|
||||||
"log_out_all_devices": "Log Out All Devices",
|
"log_out_all_devices": "Log Out All Devices",
|
||||||
"logged_out_all_devices": "Logged 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_only_show_favorites": "Show Favorite Only",
|
||||||
"map_settings_theme_settings": "Map Theme",
|
"map_settings_theme_settings": "Map Theme",
|
||||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
"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_all_as_read": "Mark all as read",
|
||||||
|
"mark_as_read": "Mark as read",
|
||||||
"marked_all_as_read": "Marked all as read",
|
"marked_all_as_read": "Marked all as read",
|
||||||
"matches": "Matches",
|
"matches": "Matches",
|
||||||
"media_type": "Media type",
|
"media_type": "Media type",
|
||||||
@ -1242,6 +1241,10 @@
|
|||||||
"month": "Month",
|
"month": "Month",
|
||||||
"monthly_title_text_date_format": "MMMM y",
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
"more": "More",
|
"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_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
|
||||||
"moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library",
|
"moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library",
|
||||||
"moved_to_trash": "Moved to trash",
|
"moved_to_trash": "Moved to trash",
|
||||||
@ -1258,6 +1261,8 @@
|
|||||||
"new_api_key": "New API Key",
|
"new_api_key": "New API Key",
|
||||||
"new_password": "New password",
|
"new_password": "New password",
|
||||||
"new_person": "New person",
|
"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_user_created": "New user created",
|
||||||
"new_version_available": "NEW VERSION AVAILABLE",
|
"new_version_available": "NEW VERSION AVAILABLE",
|
||||||
"newest_first": "Newest first",
|
"newest_first": "Newest first",
|
||||||
@ -1275,23 +1280,24 @@
|
|||||||
"no_explore_results_message": "Upload more photos to explore your collection.",
|
"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_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_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_name": "No Name",
|
||||||
|
"no_notifications": "No notifications",
|
||||||
"no_people_found": "No matching people found",
|
"no_people_found": "No matching people found",
|
||||||
"no_places": "No places",
|
"no_places": "No places",
|
||||||
"no_results": "No results",
|
"no_results": "No results",
|
||||||
"no_results_description": "Try a synonym or more general keyword",
|
"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",
|
"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_in_any_album": "Not in any album",
|
||||||
"not_selected": "Not selected",
|
"not_selected": "Not selected",
|
||||||
"note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
"note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the",
|
||||||
"notes": "Notes",
|
"notes": "Notes",
|
||||||
|
"nothing_here_yet": "Nothing here yet",
|
||||||
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
"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_content": "Grant permission to enable notifications.",
|
||||||
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
"notification_permission_list_tile_enable_button": "Enable Notifications",
|
||||||
"notification_permission_list_tile_title": "Notification Permission",
|
"notification_permission_list_tile_title": "Notification Permission",
|
||||||
"notification_toggle_setting_description": "Enable email notifications",
|
"notification_toggle_setting_description": "Enable email notifications",
|
||||||
"email_notifications": "Email notifications",
|
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"notifications_setting_description": "Manage notifications",
|
"notifications_setting_description": "Manage notifications",
|
||||||
"oauth": "OAuth",
|
"oauth": "OAuth",
|
||||||
@ -1379,6 +1385,10 @@
|
|||||||
"photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}",
|
"photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}",
|
||||||
"photos_from_previous_years": "Photos from previous years",
|
"photos_from_previous_years": "Photos from previous years",
|
||||||
"pick_a_location": "Pick a location",
|
"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",
|
"place": "Place",
|
||||||
"places": "Places",
|
"places": "Places",
|
||||||
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
|
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
|
||||||
@ -1476,6 +1486,8 @@
|
|||||||
"remove_deleted_assets": "Remove Deleted Assets",
|
"remove_deleted_assets": "Remove Deleted Assets",
|
||||||
"remove_from_album": "Remove from album",
|
"remove_from_album": "Remove from album",
|
||||||
"remove_from_favorites": "Remove from favorites",
|
"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_from_shared_link": "Remove from shared link",
|
||||||
"remove_memory": "Remove memory",
|
"remove_memory": "Remove memory",
|
||||||
"remove_photo_from_memory": "Remove photo from this memory",
|
"remove_photo_from_memory": "Remove photo from this memory",
|
||||||
@ -1499,6 +1511,7 @@
|
|||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"reset_password": "Reset password",
|
"reset_password": "Reset password",
|
||||||
"reset_people_visibility": "Reset people visibility",
|
"reset_people_visibility": "Reset people visibility",
|
||||||
|
"reset_pin_code": "Reset PIN code",
|
||||||
"reset_to_default": "Reset to default",
|
"reset_to_default": "Reset to default",
|
||||||
"resolve_duplicates": "Resolve duplicates",
|
"resolve_duplicates": "Resolve duplicates",
|
||||||
"resolved_all_duplicates": "Resolved all duplicates",
|
"resolved_all_duplicates": "Resolved all duplicates",
|
||||||
@ -1639,6 +1652,7 @@
|
|||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"settings_require_restart": "Please restart Immich to apply this setting",
|
"settings_require_restart": "Please restart Immich to apply this setting",
|
||||||
"settings_saved": "Settings saved",
|
"settings_saved": "Settings saved",
|
||||||
|
"setup_pin_code": "Setup a PIN code",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
"share_assets_selected": "{count} selected",
|
"share_assets_selected": "{count} selected",
|
||||||
@ -1755,8 +1769,8 @@
|
|||||||
"stop_sharing_photos_with_user": "Stop sharing your photos with this user",
|
"stop_sharing_photos_with_user": "Stop sharing your photos with this user",
|
||||||
"storage": "Storage space",
|
"storage": "Storage space",
|
||||||
"storage_label": "Storage label",
|
"storage_label": "Storage label",
|
||||||
"storage_usage": "{used} of {available} used",
|
|
||||||
"storage_quota": "Storage Quota",
|
"storage_quota": "Storage Quota",
|
||||||
|
"storage_usage": "{used} of {available} used",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "Suggestions",
|
||||||
"sunrise_on_the_beach": "Sunrise on the beach",
|
"sunrise_on_the_beach": "Sunrise on the beach",
|
||||||
@ -1824,6 +1838,8 @@
|
|||||||
"trash_page_title": "Trash ({count})",
|
"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}}.",
|
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
|
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||||
|
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||||
"unarchive": "Unarchive",
|
"unarchive": "Unarchive",
|
||||||
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
||||||
"unfavorite": "Unfavorite",
|
"unfavorite": "Unfavorite",
|
||||||
@ -1847,6 +1863,7 @@
|
|||||||
"untracked_files": "Untracked files",
|
"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",
|
"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",
|
"up_next": "Up next",
|
||||||
|
"updated_at": "Updated",
|
||||||
"updated_password": "Updated password",
|
"updated_password": "Updated password",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"upload_concurrency": "Upload concurrency",
|
"upload_concurrency": "Upload concurrency",
|
||||||
@ -1861,7 +1878,6 @@
|
|||||||
"upload_success": "Upload success, refresh the page to see new upload assets.",
|
"upload_success": "Upload success, refresh the page to see new upload assets.",
|
||||||
"upload_to_immich": "Upload to Immich ({count})",
|
"upload_to_immich": "Upload to Immich ({count})",
|
||||||
"uploading": "Uploading",
|
"uploading": "Uploading",
|
||||||
"id": "ID",
|
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
"use_current_connection": "use current connection",
|
"use_current_connection": "use current connection",
|
||||||
@ -1870,8 +1886,8 @@
|
|||||||
"user_has_been_deleted": "This user has been deleted.",
|
"user_has_been_deleted": "This user has been deleted.",
|
||||||
"user_id": "User ID",
|
"user_id": "User ID",
|
||||||
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
||||||
"created_at": "Created",
|
"user_pin_code_settings": "PIN Code",
|
||||||
"updated_at": "Updated",
|
"user_pin_code_settings_description": "Manage your PIN code",
|
||||||
"user_purchase_settings": "Purchase",
|
"user_purchase_settings": "Purchase",
|
||||||
"user_purchase_settings_description": "Manage your purchase",
|
"user_purchase_settings_description": "Manage your purchase",
|
||||||
"user_role_set": "Set {user} as {role}",
|
"user_role_set": "Set {user} as {role}",
|
||||||
@ -1921,6 +1937,7 @@
|
|||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
"welcome_to_immich": "Welcome to Immich",
|
"welcome_to_immich": "Welcome to Immich",
|
||||||
"wifi_name": "Wi-Fi Name",
|
"wifi_name": "Wi-Fi Name",
|
||||||
|
"wrong_pin_code": "Wrong PIN code",
|
||||||
"year": "Year",
|
"year": "Year",
|
||||||
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
|
@ -29,6 +29,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
case 'UserResponseDto':
|
case 'UserResponseDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||||
|
addDefault(value, 'visibility', AssetVisibility.timeline);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'UserAdminResponseDto':
|
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* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||||
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code |
|
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code |
|
||||||
*AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status |
|
*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* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login |
|
||||||
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
*AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout |
|
||||||
*AuthenticationApi* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code |
|
*AuthenticationApi* | [**resetPinCode**](doc//AuthenticationApi.md#resetpincode) | **DELETE** /auth/pin-code |
|
||||||
*AuthenticationApi* | [**setupPinCode**](doc//AuthenticationApi.md#setuppincode) | **POST** /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* | [**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 |
|
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||||
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
||||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
*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* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history |
|
||||||
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
|
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
|
||||||
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
|
*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* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
|
||||||
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
|
*SessionsApi* | [**deleteSession**](doc//SessionsApi.md#deletesession) | **DELETE** /sessions/{id} |
|
||||||
*SessionsApi* | [**getSessions**](doc//SessionsApi.md#getsessions) | **GET** /sessions |
|
*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* | [**addSharedLinkAssets**](doc//SharedLinksApi.md#addsharedlinkassets) | **PUT** /shared-links/{id}/assets |
|
||||||
*SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |
|
*SharedLinksApi* | [**createSharedLink**](doc//SharedLinksApi.md#createsharedlink) | **POST** /shared-links |
|
||||||
*SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |
|
*SharedLinksApi* | [**getAllSharedLinks**](doc//SharedLinksApi.md#getallsharedlinks) | **GET** /shared-links |
|
||||||
@ -390,6 +394,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
||||||
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
|
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
|
||||||
- [PinCodeChangeDto](doc//PinCodeChangeDto.md)
|
- [PinCodeChangeDto](doc//PinCodeChangeDto.md)
|
||||||
|
- [PinCodeResetDto](doc//PinCodeResetDto.md)
|
||||||
- [PinCodeSetupDto](doc//PinCodeSetupDto.md)
|
- [PinCodeSetupDto](doc//PinCodeSetupDto.md)
|
||||||
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
||||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||||
@ -419,7 +424,10 @@ Class | Method | HTTP request | Description
|
|||||||
- [ServerThemeDto](doc//ServerThemeDto.md)
|
- [ServerThemeDto](doc//ServerThemeDto.md)
|
||||||
- [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md)
|
- [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md)
|
||||||
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
||||||
|
- [SessionCreateDto](doc//SessionCreateDto.md)
|
||||||
|
- [SessionCreateResponseDto](doc//SessionCreateResponseDto.md)
|
||||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
- [SessionResponseDto](doc//SessionResponseDto.md)
|
||||||
|
- [SessionUnlockDto](doc//SessionUnlockDto.md)
|
||||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||||
|
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_update_dto.dart';
|
||||||
part 'model/person_with_faces_response_dto.dart';
|
part 'model/person_with_faces_response_dto.dart';
|
||||||
part 'model/pin_code_change_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/pin_code_setup_dto.dart';
|
||||||
part 'model/places_response_dto.dart';
|
part 'model/places_response_dto.dart';
|
||||||
part 'model/purchase_response.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_theme_dto.dart';
|
||||||
part 'model/server_version_history_response_dto.dart';
|
part 'model/server_version_history_response_dto.dart';
|
||||||
part 'model/server_version_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_response_dto.dart';
|
||||||
|
part 'model/session_unlock_dto.dart';
|
||||||
part 'model/shared_link_create_dto.dart';
|
part 'model/shared_link_create_dto.dart';
|
||||||
part 'model/shared_link_edit_dto.dart';
|
part 'model/shared_link_edit_dto.dart';
|
||||||
part 'model/shared_link_response_dto.dart';
|
part 'model/shared_link_response_dto.dart';
|
||||||
|
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;
|
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].
|
/// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
@ -234,13 +267,13 @@ class AuthenticationApi {
|
|||||||
/// Performs an HTTP 'DELETE /auth/pin-code' operation and returns the [Response].
|
/// Performs an HTTP 'DELETE /auth/pin-code' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [PinCodeChangeDto] pinCodeChangeDto (required):
|
/// * [PinCodeResetDto] pinCodeResetDto (required):
|
||||||
Future<Response> resetPinCodeWithHttpInfo(PinCodeChangeDto pinCodeChangeDto,) async {
|
Future<Response> resetPinCodeWithHttpInfo(PinCodeResetDto pinCodeResetDto,) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/auth/pin-code';
|
final apiPath = r'/auth/pin-code';
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
// ignore: prefer_final_locals
|
||||||
Object? postBody = pinCodeChangeDto;
|
Object? postBody = pinCodeResetDto;
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
final queryParams = <QueryParam>[];
|
||||||
final headerParams = <String, String>{};
|
final headerParams = <String, String>{};
|
||||||
@ -262,9 +295,9 @@ class AuthenticationApi {
|
|||||||
|
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [PinCodeChangeDto] pinCodeChangeDto (required):
|
/// * [PinCodeResetDto] pinCodeResetDto (required):
|
||||||
Future<void> resetPinCode(PinCodeChangeDto pinCodeChangeDto,) async {
|
Future<void> resetPinCode(PinCodeResetDto pinCodeResetDto,) async {
|
||||||
final response = await resetPinCodeWithHttpInfo(pinCodeChangeDto,);
|
final response = await resetPinCodeWithHttpInfo(pinCodeResetDto,);
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
@ -356,6 +389,45 @@ class AuthenticationApi {
|
|||||||
return null;
|
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].
|
/// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response].
|
||||||
Future<Response> validateAccessTokenWithHttpInfo() async {
|
Future<Response> validateAccessTokenWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// 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;
|
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].
|
/// Performs an HTTP 'DELETE /sessions' operation and returns the [Response].
|
||||||
Future<Response> deleteAllSessionsWithHttpInfo() async {
|
Future<Response> deleteAllSessionsWithHttpInfo() async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
@ -132,4 +179,44 @@ class SessionsApi {
|
|||||||
}
|
}
|
||||||
return null;
|
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);
|
return PersonWithFacesResponseDto.fromJson(value);
|
||||||
case 'PinCodeChangeDto':
|
case 'PinCodeChangeDto':
|
||||||
return PinCodeChangeDto.fromJson(value);
|
return PinCodeChangeDto.fromJson(value);
|
||||||
|
case 'PinCodeResetDto':
|
||||||
|
return PinCodeResetDto.fromJson(value);
|
||||||
case 'PinCodeSetupDto':
|
case 'PinCodeSetupDto':
|
||||||
return PinCodeSetupDto.fromJson(value);
|
return PinCodeSetupDto.fromJson(value);
|
||||||
case 'PlacesResponseDto':
|
case 'PlacesResponseDto':
|
||||||
@ -492,8 +494,14 @@ class ApiClient {
|
|||||||
return ServerVersionHistoryResponseDto.fromJson(value);
|
return ServerVersionHistoryResponseDto.fromJson(value);
|
||||||
case 'ServerVersionResponseDto':
|
case 'ServerVersionResponseDto':
|
||||||
return ServerVersionResponseDto.fromJson(value);
|
return ServerVersionResponseDto.fromJson(value);
|
||||||
|
case 'SessionCreateDto':
|
||||||
|
return SessionCreateDto.fromJson(value);
|
||||||
|
case 'SessionCreateResponseDto':
|
||||||
|
return SessionCreateResponseDto.fromJson(value);
|
||||||
case 'SessionResponseDto':
|
case 'SessionResponseDto':
|
||||||
return SessionResponseDto.fromJson(value);
|
return SessionResponseDto.fromJson(value);
|
||||||
|
case 'SessionUnlockDto':
|
||||||
|
return SessionUnlockDto.fromJson(value);
|
||||||
case 'SharedLinkCreateDto':
|
case 'SharedLinkCreateDto':
|
||||||
return SharedLinkCreateDto.fromJson(value);
|
return SharedLinkCreateDto.fromJson(value);
|
||||||
case 'SharedLinkEditDto':
|
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,
|
required this.type,
|
||||||
this.unassignedFaces = const [],
|
this.unassignedFaces = const [],
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
|
required this.visibility,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// base64 encoded sha1 hash
|
/// base64 encoded sha1 hash
|
||||||
@ -132,6 +133,8 @@ class AssetResponseDto {
|
|||||||
|
|
||||||
DateTime updatedAt;
|
DateTime updatedAt;
|
||||||
|
|
||||||
|
AssetResponseDtoVisibilityEnum visibility;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||||
other.checksum == checksum &&
|
other.checksum == checksum &&
|
||||||
@ -163,7 +166,8 @@ class AssetResponseDto {
|
|||||||
other.thumbhash == thumbhash &&
|
other.thumbhash == thumbhash &&
|
||||||
other.type == type &&
|
other.type == type &&
|
||||||
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
|
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
|
||||||
other.updatedAt == updatedAt;
|
other.updatedAt == updatedAt &&
|
||||||
|
other.visibility == visibility;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -197,10 +201,11 @@ class AssetResponseDto {
|
|||||||
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(unassignedFaces.hashCode) +
|
(unassignedFaces.hashCode) +
|
||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode) +
|
||||||
|
(visibility.hashCode);
|
||||||
|
|
||||||
@override
|
@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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -270,6 +275,7 @@ class AssetResponseDto {
|
|||||||
json[r'type'] = this.type;
|
json[r'type'] = this.type;
|
||||||
json[r'unassignedFaces'] = this.unassignedFaces;
|
json[r'unassignedFaces'] = this.unassignedFaces;
|
||||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||||
|
json[r'visibility'] = this.visibility;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,6 +318,7 @@ class AssetResponseDto {
|
|||||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
||||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||||
|
visibility: AssetResponseDtoVisibilityEnum.fromJson(json[r'visibility'])!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -378,6 +385,87 @@ class AssetResponseDto {
|
|||||||
'thumbhash',
|
'thumbhash',
|
||||||
'type',
|
'type',
|
||||||
'updatedAt',
|
'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 archive = AssetVisibility._(r'archive');
|
||||||
static const timeline = AssetVisibility._(r'timeline');
|
static const timeline = AssetVisibility._(r'timeline');
|
||||||
static const hidden = AssetVisibility._(r'hidden');
|
static const hidden = AssetVisibility._(r'hidden');
|
||||||
|
static const locked = AssetVisibility._(r'locked');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][AssetVisibility].
|
/// List of all possible values in this [enum][AssetVisibility].
|
||||||
static const values = <AssetVisibility>[
|
static const values = <AssetVisibility>[
|
||||||
archive,
|
archive,
|
||||||
timeline,
|
timeline,
|
||||||
hidden,
|
hidden,
|
||||||
|
locked,
|
||||||
];
|
];
|
||||||
|
|
||||||
static AssetVisibility? fromJson(dynamic value) => AssetVisibilityTypeTransformer().decode(value);
|
static AssetVisibility? fromJson(dynamic value) => AssetVisibilityTypeTransformer().decode(value);
|
||||||
@ -73,6 +75,7 @@ class AssetVisibilityTypeTransformer {
|
|||||||
case r'archive': return AssetVisibility.archive;
|
case r'archive': return AssetVisibility.archive;
|
||||||
case r'timeline': return AssetVisibility.timeline;
|
case r'timeline': return AssetVisibility.timeline;
|
||||||
case r'hidden': return AssetVisibility.hidden;
|
case r'hidden': return AssetVisibility.hidden;
|
||||||
|
case r'locked': return AssetVisibility.locked;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
@ -13,32 +13,70 @@ part of openapi.api;
|
|||||||
class AuthStatusResponseDto {
|
class AuthStatusResponseDto {
|
||||||
/// Returns a new [AuthStatusResponseDto] instance.
|
/// Returns a new [AuthStatusResponseDto] instance.
|
||||||
AuthStatusResponseDto({
|
AuthStatusResponseDto({
|
||||||
|
this.expiresAt,
|
||||||
|
required this.isElevated,
|
||||||
required this.password,
|
required this.password,
|
||||||
required this.pinCode,
|
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 password;
|
||||||
|
|
||||||
bool pinCode;
|
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
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AuthStatusResponseDto &&
|
||||||
|
other.expiresAt == expiresAt &&
|
||||||
|
other.isElevated == isElevated &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
other.pinCode == pinCode;
|
other.pinCode == pinCode &&
|
||||||
|
other.pinExpiresAt == pinExpiresAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
|
(isElevated.hashCode) +
|
||||||
(password.hashCode) +
|
(password.hashCode) +
|
||||||
(pinCode.hashCode);
|
(pinCode.hashCode) +
|
||||||
|
(pinExpiresAt == null ? 0 : pinExpiresAt!.hashCode);
|
||||||
|
|
||||||
@override
|
@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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
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'password'] = this.password;
|
||||||
json[r'pinCode'] = this.pinCode;
|
json[r'pinCode'] = this.pinCode;
|
||||||
|
if (this.pinExpiresAt != null) {
|
||||||
|
json[r'pinExpiresAt'] = this.pinExpiresAt;
|
||||||
|
} else {
|
||||||
|
// json[r'pinExpiresAt'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,8 +89,11 @@ class AuthStatusResponseDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return AuthStatusResponseDto(
|
return AuthStatusResponseDto(
|
||||||
|
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||||
|
isElevated: mapValueOfType<bool>(json, r'isElevated')!,
|
||||||
password: mapValueOfType<bool>(json, r'password')!,
|
password: mapValueOfType<bool>(json, r'password')!,
|
||||||
pinCode: mapValueOfType<bool>(json, r'pinCode')!,
|
pinCode: mapValueOfType<bool>(json, r'pinCode')!,
|
||||||
|
pinExpiresAt: mapValueOfType<String>(json, r'pinExpiresAt'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -100,6 +141,7 @@ class AuthStatusResponseDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
|
'isElevated',
|
||||||
'password',
|
'password',
|
||||||
'pinCode',
|
'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 personPeriodStatistics = Permission._(r'person.statistics');
|
||||||
static const personPeriodMerge = Permission._(r'person.merge');
|
static const personPeriodMerge = Permission._(r'person.merge');
|
||||||
static const personPeriodReassign = Permission._(r'person.reassign');
|
static const personPeriodReassign = Permission._(r'person.reassign');
|
||||||
|
static const sessionPeriodCreate = Permission._(r'session.create');
|
||||||
static const sessionPeriodRead = Permission._(r'session.read');
|
static const sessionPeriodRead = Permission._(r'session.read');
|
||||||
static const sessionPeriodUpdate = Permission._(r'session.update');
|
static const sessionPeriodUpdate = Permission._(r'session.update');
|
||||||
static const sessionPeriodDelete = Permission._(r'session.delete');
|
static const sessionPeriodDelete = Permission._(r'session.delete');
|
||||||
|
static const sessionPeriodLock = Permission._(r'session.lock');
|
||||||
static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create');
|
static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create');
|
||||||
static const sharedLinkPeriodRead = Permission._(r'sharedLink.read');
|
static const sharedLinkPeriodRead = Permission._(r'sharedLink.read');
|
||||||
static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update');
|
static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update');
|
||||||
@ -166,9 +168,11 @@ class Permission {
|
|||||||
personPeriodStatistics,
|
personPeriodStatistics,
|
||||||
personPeriodMerge,
|
personPeriodMerge,
|
||||||
personPeriodReassign,
|
personPeriodReassign,
|
||||||
|
sessionPeriodCreate,
|
||||||
sessionPeriodRead,
|
sessionPeriodRead,
|
||||||
sessionPeriodUpdate,
|
sessionPeriodUpdate,
|
||||||
sessionPeriodDelete,
|
sessionPeriodDelete,
|
||||||
|
sessionPeriodLock,
|
||||||
sharedLinkPeriodCreate,
|
sharedLinkPeriodCreate,
|
||||||
sharedLinkPeriodRead,
|
sharedLinkPeriodRead,
|
||||||
sharedLinkPeriodUpdate,
|
sharedLinkPeriodUpdate,
|
||||||
@ -286,9 +290,11 @@ class PermissionTypeTransformer {
|
|||||||
case r'person.statistics': return Permission.personPeriodStatistics;
|
case r'person.statistics': return Permission.personPeriodStatistics;
|
||||||
case r'person.merge': return Permission.personPeriodMerge;
|
case r'person.merge': return Permission.personPeriodMerge;
|
||||||
case r'person.reassign': return Permission.personPeriodReassign;
|
case r'person.reassign': return Permission.personPeriodReassign;
|
||||||
|
case r'session.create': return Permission.sessionPeriodCreate;
|
||||||
case r'session.read': return Permission.sessionPeriodRead;
|
case r'session.read': return Permission.sessionPeriodRead;
|
||||||
case r'session.update': return Permission.sessionPeriodUpdate;
|
case r'session.update': return Permission.sessionPeriodUpdate;
|
||||||
case r'session.delete': return Permission.sessionPeriodDelete;
|
case r'session.delete': return Permission.sessionPeriodDelete;
|
||||||
|
case r'session.lock': return Permission.sessionPeriodLock;
|
||||||
case r'sharedLink.create': return Permission.sharedLinkPeriodCreate;
|
case r'sharedLink.create': return Permission.sharedLinkPeriodCreate;
|
||||||
case r'sharedLink.read': return Permission.sharedLinkPeriodRead;
|
case r'sharedLink.read': return Permission.sharedLinkPeriodRead;
|
||||||
case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate;
|
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.current,
|
||||||
required this.deviceOS,
|
required this.deviceOS,
|
||||||
required this.deviceType,
|
required this.deviceType,
|
||||||
|
this.expiresAt,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
@ -29,6 +30,14 @@ class SessionResponseDto {
|
|||||||
|
|
||||||
String deviceType;
|
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 id;
|
||||||
|
|
||||||
String updatedAt;
|
String updatedAt;
|
||||||
@ -39,6 +48,7 @@ class SessionResponseDto {
|
|||||||
other.current == current &&
|
other.current == current &&
|
||||||
other.deviceOS == deviceOS &&
|
other.deviceOS == deviceOS &&
|
||||||
other.deviceType == deviceType &&
|
other.deviceType == deviceType &&
|
||||||
|
other.expiresAt == expiresAt &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.updatedAt == updatedAt;
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
@ -49,11 +59,12 @@ class SessionResponseDto {
|
|||||||
(current.hashCode) +
|
(current.hashCode) +
|
||||||
(deviceOS.hashCode) +
|
(deviceOS.hashCode) +
|
||||||
(deviceType.hashCode) +
|
(deviceType.hashCode) +
|
||||||
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -61,6 +72,11 @@ class SessionResponseDto {
|
|||||||
json[r'current'] = this.current;
|
json[r'current'] = this.current;
|
||||||
json[r'deviceOS'] = this.deviceOS;
|
json[r'deviceOS'] = this.deviceOS;
|
||||||
json[r'deviceType'] = this.deviceType;
|
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'id'] = this.id;
|
||||||
json[r'updatedAt'] = this.updatedAt;
|
json[r'updatedAt'] = this.updatedAt;
|
||||||
return json;
|
return json;
|
||||||
@ -79,6 +95,7 @@ class SessionResponseDto {
|
|||||||
current: mapValueOfType<bool>(json, r'current')!,
|
current: mapValueOfType<bool>(json, r'current')!,
|
||||||
deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
|
deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
|
||||||
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
deviceType: mapValueOfType<String>(json, r'deviceType')!,
|
||||||
|
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
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 archive = SyncAssetV1VisibilityEnum._(r'archive');
|
||||||
static const timeline = SyncAssetV1VisibilityEnum._(r'timeline');
|
static const timeline = SyncAssetV1VisibilityEnum._(r'timeline');
|
||||||
static const hidden = SyncAssetV1VisibilityEnum._(r'hidden');
|
static const hidden = SyncAssetV1VisibilityEnum._(r'hidden');
|
||||||
|
static const locked = SyncAssetV1VisibilityEnum._(r'locked');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SyncAssetV1VisibilityEnum].
|
/// List of all possible values in this [enum][SyncAssetV1VisibilityEnum].
|
||||||
static const values = <SyncAssetV1VisibilityEnum>[
|
static const values = <SyncAssetV1VisibilityEnum>[
|
||||||
archive,
|
archive,
|
||||||
timeline,
|
timeline,
|
||||||
hidden,
|
hidden,
|
||||||
|
locked,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value);
|
static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value);
|
||||||
@ -340,6 +342,7 @@ class SyncAssetV1VisibilityEnumTypeTransformer {
|
|||||||
case r'archive': return SyncAssetV1VisibilityEnum.archive;
|
case r'archive': return SyncAssetV1VisibilityEnum.archive;
|
||||||
case r'timeline': return SyncAssetV1VisibilityEnum.timeline;
|
case r'timeline': return SyncAssetV1VisibilityEnum.timeline;
|
||||||
case r'hidden': return SyncAssetV1VisibilityEnum.hidden;
|
case r'hidden': return SyncAssetV1VisibilityEnum.hidden;
|
||||||
|
case r'locked': return SyncAssetV1VisibilityEnum.locked;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
@ -2377,7 +2377,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"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": {
|
"/auth/status": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAuthStatus",
|
"operationId": "getAuthStatus",
|
||||||
@ -5583,6 +5643,46 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Sessions"
|
"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}": {
|
"/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": {
|
"/shared-links": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAllSharedLinks",
|
"operationId": "getAllSharedLinks",
|
||||||
@ -9150,6 +9285,15 @@
|
|||||||
"updatedAt": {
|
"updatedAt": {
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"enum": [
|
||||||
|
"archive",
|
||||||
|
"timeline",
|
||||||
|
"hidden",
|
||||||
|
"locked"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -9171,7 +9315,8 @@
|
|||||||
"ownerId",
|
"ownerId",
|
||||||
"thumbhash",
|
"thumbhash",
|
||||||
"type",
|
"type",
|
||||||
"updatedAt"
|
"updatedAt",
|
||||||
|
"visibility"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@ -9226,7 +9371,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"archive",
|
"archive",
|
||||||
"timeline",
|
"timeline",
|
||||||
"hidden"
|
"hidden",
|
||||||
|
"locked"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -9241,14 +9387,24 @@
|
|||||||
},
|
},
|
||||||
"AuthStatusResponseDto": {
|
"AuthStatusResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isElevated": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"pinCode": {
|
"pinCode": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pinExpiresAt": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
"isElevated",
|
||||||
"password",
|
"password",
|
||||||
"pinCode"
|
"pinCode"
|
||||||
],
|
],
|
||||||
@ -11002,9 +11158,11 @@
|
|||||||
"person.statistics",
|
"person.statistics",
|
||||||
"person.merge",
|
"person.merge",
|
||||||
"person.reassign",
|
"person.reassign",
|
||||||
|
"session.create",
|
||||||
"session.read",
|
"session.read",
|
||||||
"session.update",
|
"session.update",
|
||||||
"session.delete",
|
"session.delete",
|
||||||
|
"session.lock",
|
||||||
"sharedLink.create",
|
"sharedLink.create",
|
||||||
"sharedLink.read",
|
"sharedLink.read",
|
||||||
"sharedLink.update",
|
"sharedLink.update",
|
||||||
@ -11206,6 +11364,18 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"PinCodeResetDto": {
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"example": "123456",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"PinCodeSetupDto": {
|
"PinCodeSetupDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"pinCode": {
|
"pinCode": {
|
||||||
@ -11988,6 +12158,60 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"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": {
|
"SessionResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
@ -12002,6 +12226,9 @@
|
|||||||
"deviceType": {
|
"deviceType": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -12019,6 +12246,18 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SessionUnlockDto": {
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"example": "123456",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SharedLinkCreateDto": {
|
"SharedLinkCreateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"albumId": {
|
"albumId": {
|
||||||
@ -12664,7 +12903,8 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"archive",
|
"archive",
|
||||||
"timeline",
|
"timeline",
|
||||||
"hidden"
|
"hidden",
|
||||||
|
"locked"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -329,6 +329,7 @@ export type AssetResponseDto = {
|
|||||||
"type": AssetTypeEnum;
|
"type": AssetTypeEnum;
|
||||||
unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
|
unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
visibility: Visibility;
|
||||||
};
|
};
|
||||||
export type AlbumResponseDto = {
|
export type AlbumResponseDto = {
|
||||||
albumName: string;
|
albumName: string;
|
||||||
@ -511,17 +512,28 @@ export type LogoutResponseDto = {
|
|||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
successful: boolean;
|
successful: boolean;
|
||||||
};
|
};
|
||||||
export type PinCodeChangeDto = {
|
export type PinCodeResetDto = {
|
||||||
newPinCode: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
pinCode?: string;
|
pinCode?: string;
|
||||||
};
|
};
|
||||||
export type PinCodeSetupDto = {
|
export type PinCodeSetupDto = {
|
||||||
pinCode: string;
|
pinCode: string;
|
||||||
};
|
};
|
||||||
|
export type PinCodeChangeDto = {
|
||||||
|
newPinCode: string;
|
||||||
|
password?: string;
|
||||||
|
pinCode?: string;
|
||||||
|
};
|
||||||
|
export type SessionUnlockDto = {
|
||||||
|
password?: string;
|
||||||
|
pinCode?: string;
|
||||||
|
};
|
||||||
export type AuthStatusResponseDto = {
|
export type AuthStatusResponseDto = {
|
||||||
|
expiresAt?: string;
|
||||||
|
isElevated: boolean;
|
||||||
password: boolean;
|
password: boolean;
|
||||||
pinCode: boolean;
|
pinCode: boolean;
|
||||||
|
pinExpiresAt?: string;
|
||||||
};
|
};
|
||||||
export type ValidateAccessTokenResponseDto = {
|
export type ValidateAccessTokenResponseDto = {
|
||||||
authStatus: boolean;
|
authStatus: boolean;
|
||||||
@ -1073,9 +1085,26 @@ export type SessionResponseDto = {
|
|||||||
current: boolean;
|
current: boolean;
|
||||||
deviceOS: string;
|
deviceOS: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
|
expiresAt?: string;
|
||||||
id: string;
|
id: string;
|
||||||
updatedAt: 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 = {
|
export type SharedLinkResponseDto = {
|
||||||
album?: AlbumResponseDto;
|
album?: AlbumResponseDto;
|
||||||
allowDownload: boolean;
|
allowDownload: boolean;
|
||||||
@ -2049,13 +2078,13 @@ export function logout(opts?: Oazapfts.RequestOpts) {
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function resetPinCode({ pinCodeChangeDto }: {
|
export function resetPinCode({ pinCodeResetDto }: {
|
||||||
pinCodeChangeDto: PinCodeChangeDto;
|
pinCodeResetDto: PinCodeResetDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
|
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: pinCodeChangeDto
|
body: pinCodeResetDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function setupPinCode({ pinCodeSetupDto }: {
|
export function setupPinCode({ pinCodeSetupDto }: {
|
||||||
@ -2076,6 +2105,21 @@ export function changePinCode({ pinCodeChangeDto }: {
|
|||||||
body: 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) {
|
export function getAuthStatus(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
@ -2906,6 +2950,18 @@ export function getSessions(opts?: Oazapfts.RequestOpts) {
|
|||||||
...opts
|
...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 }: {
|
export function deleteSession({ id }: {
|
||||||
id: string;
|
id: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
@ -2914,6 +2970,14 @@ export function deleteSession({ id }: {
|
|||||||
method: "DELETE"
|
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 }: {
|
export function getAllSharedLinks({ albumId }: {
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
@ -3574,7 +3638,8 @@ export enum UserStatus {
|
|||||||
export enum AssetVisibility {
|
export enum AssetVisibility {
|
||||||
Archive = "archive",
|
Archive = "archive",
|
||||||
Timeline = "timeline",
|
Timeline = "timeline",
|
||||||
Hidden = "hidden"
|
Hidden = "hidden",
|
||||||
|
Locked = "locked"
|
||||||
}
|
}
|
||||||
export enum AlbumUserRole {
|
export enum AlbumUserRole {
|
||||||
Editor = "editor",
|
Editor = "editor",
|
||||||
@ -3591,6 +3656,12 @@ export enum AssetTypeEnum {
|
|||||||
Audio = "AUDIO",
|
Audio = "AUDIO",
|
||||||
Other = "OTHER"
|
Other = "OTHER"
|
||||||
}
|
}
|
||||||
|
export enum Visibility {
|
||||||
|
Archive = "archive",
|
||||||
|
Timeline = "timeline",
|
||||||
|
Hidden = "hidden",
|
||||||
|
Locked = "locked"
|
||||||
|
}
|
||||||
export enum AssetOrder {
|
export enum AssetOrder {
|
||||||
Asc = "asc",
|
Asc = "asc",
|
||||||
Desc = "desc"
|
Desc = "desc"
|
||||||
@ -3660,9 +3731,11 @@ export enum Permission {
|
|||||||
PersonStatistics = "person.statistics",
|
PersonStatistics = "person.statistics",
|
||||||
PersonMerge = "person.merge",
|
PersonMerge = "person.merge",
|
||||||
PersonReassign = "person.reassign",
|
PersonReassign = "person.reassign",
|
||||||
|
SessionCreate = "session.create",
|
||||||
SessionRead = "session.read",
|
SessionRead = "session.read",
|
||||||
SessionUpdate = "session.update",
|
SessionUpdate = "session.update",
|
||||||
SessionDelete = "session.delete",
|
SessionDelete = "session.delete",
|
||||||
|
SessionLock = "session.lock",
|
||||||
SharedLinkCreate = "sharedLink.create",
|
SharedLinkCreate = "sharedLink.create",
|
||||||
SharedLinkRead = "sharedLink.read",
|
SharedLinkRead = "sharedLink.read",
|
||||||
SharedLinkUpdate = "sharedLink.update",
|
SharedLinkUpdate = "sharedLink.update",
|
||||||
|
@ -9,7 +9,9 @@ import {
|
|||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
LogoutResponseDto,
|
LogoutResponseDto,
|
||||||
PinCodeChangeDto,
|
PinCodeChangeDto,
|
||||||
|
PinCodeResetDto,
|
||||||
PinCodeSetupDto,
|
PinCodeSetupDto,
|
||||||
|
SessionUnlockDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
ValidateAccessTokenResponseDto,
|
ValidateAccessTokenResponseDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
@ -98,7 +100,21 @@ export class AuthController {
|
|||||||
|
|
||||||
@Delete('pin-code')
|
@Delete('pin-code')
|
||||||
@Authenticated()
|
@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);
|
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' });
|
.send({ visibility: 'immich' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(
|
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 { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
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 { Permission } from 'src/enum';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
import { SessionService } from 'src/services/session.service';
|
import { SessionService } from 'src/services/session.service';
|
||||||
@ -12,6 +12,12 @@ import { UUIDParamDto } from 'src/validation';
|
|||||||
export class SessionController {
|
export class SessionController {
|
||||||
constructor(private service: SessionService) {}
|
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()
|
@Get()
|
||||||
@Authenticated({ permission: Permission.SESSION_READ })
|
@Authenticated({ permission: Permission.SESSION_READ })
|
||||||
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
|
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
|
||||||
@ -31,4 +37,11 @@ export class SessionController {
|
|||||||
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
return this.service.delete(auth, id);
|
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 = {
|
export type AuthSession = {
|
||||||
id: string;
|
id: string;
|
||||||
|
hasElevatedPermission: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Partner = {
|
export type Partner = {
|
||||||
@ -231,8 +232,10 @@ export type Session = {
|
|||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
expiresAt: Date | null;
|
||||||
deviceOS: string;
|
deviceOS: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
|
pinExpiresAt: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Exif = Omit<Selectable<DatabaseExif>, 'updatedAt' | 'updateId'>;
|
export type Exif = Omit<Selectable<DatabaseExif>, 'updatedAt' | 'updateId'>;
|
||||||
@ -306,7 +309,7 @@ export const columns = {
|
|||||||
'users.quotaSizeInBytes',
|
'users.quotaSizeInBytes',
|
||||||
],
|
],
|
||||||
authApiKey: ['api_keys.id', 'api_keys.permissions'],
|
authApiKey: ['api_keys.id', 'api_keys.permissions'],
|
||||||
authSession: ['sessions.id', 'sessions.updatedAt'],
|
authSession: ['sessions.id', 'sessions.updatedAt', 'sessions.pinExpiresAt'],
|
||||||
authSharedLink: [
|
authSharedLink: [
|
||||||
'shared_links.id',
|
'shared_links.id',
|
||||||
'shared_links.userId',
|
'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>;
|
deviceOS: Generated<string>;
|
||||||
deviceType: Generated<string>;
|
deviceType: Generated<string>;
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
|
parentId: string | null;
|
||||||
|
expiresAt: Date | null;
|
||||||
token: string;
|
token: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
updateId: Generated<string>;
|
updateId: Generated<string>;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
pinExpiresAt: Timestamp | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionSyncCheckpoints {
|
export interface SessionSyncCheckpoints {
|
||||||
|
@ -43,6 +43,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||||||
isArchived!: boolean;
|
isArchived!: boolean;
|
||||||
isTrashed!: boolean;
|
isTrashed!: boolean;
|
||||||
isOffline!: boolean;
|
isOffline!: boolean;
|
||||||
|
visibility!: AssetVisibility;
|
||||||
exifInfo?: ExifResponseDto;
|
exifInfo?: ExifResponseDto;
|
||||||
tags?: TagResponseDto[];
|
tags?: TagResponseDto[];
|
||||||
people?: PersonWithFacesResponseDto[];
|
people?: PersonWithFacesResponseDto[];
|
||||||
@ -184,6 +185,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
|
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
|
||||||
isArchived: entity.visibility === AssetVisibility.ARCHIVE,
|
isArchived: entity.visibility === AssetVisibility.ARCHIVE,
|
||||||
isTrashed: !!entity.deletedAt,
|
isTrashed: !!entity.deletedAt,
|
||||||
|
visibility: entity.visibility,
|
||||||
duration: entity.duration ?? '0:00:00.00000',
|
duration: entity.duration ?? '0:00:00.00000',
|
||||||
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
||||||
livePhotoVideoId: entity.livePhotoVideoId,
|
livePhotoVideoId: entity.livePhotoVideoId,
|
||||||
|
@ -93,6 +93,8 @@ export class PinCodeResetDto {
|
|||||||
password?: string;
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SessionUnlockDto extends PinCodeResetDto {}
|
||||||
|
|
||||||
export class PinCodeChangeDto extends PinCodeResetDto {
|
export class PinCodeChangeDto extends PinCodeResetDto {
|
||||||
@PinCode()
|
@PinCode()
|
||||||
newPinCode!: string;
|
newPinCode!: string;
|
||||||
@ -138,4 +140,7 @@ export class OAuthAuthorizeResponseDto {
|
|||||||
export class AuthStatusResponseDto {
|
export class AuthStatusResponseDto {
|
||||||
pinCode!: boolean;
|
pinCode!: boolean;
|
||||||
password!: 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 { 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 {
|
export class SessionResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
createdAt!: string;
|
createdAt!: string;
|
||||||
updatedAt!: string;
|
updatedAt!: string;
|
||||||
|
expiresAt?: string;
|
||||||
current!: boolean;
|
current!: boolean;
|
||||||
deviceType!: string;
|
deviceType!: string;
|
||||||
deviceOS!: string;
|
deviceOS!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SessionCreateResponseDto extends SessionResponseDto {
|
||||||
|
token!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const mapSession = (entity: Session, currentId?: string): SessionResponseDto => ({
|
export const mapSession = (entity: Session, currentId?: string): SessionResponseDto => ({
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
createdAt: entity.createdAt.toISOString(),
|
createdAt: entity.createdAt.toISOString(),
|
||||||
updatedAt: entity.updatedAt.toISOString(),
|
updatedAt: entity.updatedAt.toISOString(),
|
||||||
|
expiresAt: entity.expiresAt?.toISOString(),
|
||||||
current: currentId === entity.id,
|
current: currentId === entity.id,
|
||||||
deviceOS: entity.deviceOS,
|
deviceOS: entity.deviceOS,
|
||||||
deviceType: entity.deviceType,
|
deviceType: entity.deviceType,
|
||||||
|
@ -144,9 +144,11 @@ export enum Permission {
|
|||||||
PERSON_MERGE = 'person.merge',
|
PERSON_MERGE = 'person.merge',
|
||||||
PERSON_REASSIGN = 'person.reassign',
|
PERSON_REASSIGN = 'person.reassign',
|
||||||
|
|
||||||
|
SESSION_CREATE = 'session.create',
|
||||||
SESSION_READ = 'session.read',
|
SESSION_READ = 'session.read',
|
||||||
SESSION_UPDATE = 'session.update',
|
SESSION_UPDATE = 'session.update',
|
||||||
SESSION_DELETE = 'session.delete',
|
SESSION_DELETE = 'session.delete',
|
||||||
|
SESSION_LOCK = 'session.lock',
|
||||||
|
|
||||||
SHARED_LINK_CREATE = 'sharedLink.create',
|
SHARED_LINK_CREATE = 'sharedLink.create',
|
||||||
SHARED_LINK_READ = 'sharedLink.read',
|
SHARED_LINK_READ = 'sharedLink.read',
|
||||||
@ -627,4 +629,5 @@ export enum AssetVisibility {
|
|||||||
* Video part of the LivePhotos and MotionPhotos
|
* Video part of the LivePhotos and MotionPhotos
|
||||||
*/
|
*/
|
||||||
HIDDEN = 'hidden',
|
HIDDEN = 'hidden',
|
||||||
|
LOCKED = 'locked',
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,7 @@ from
|
|||||||
where
|
where
|
||||||
"assets"."id" in ($1)
|
"assets"."id" in ($1)
|
||||||
and "assets"."ownerId" = $2
|
and "assets"."ownerId" = $2
|
||||||
|
and "assets"."visibility" != $3
|
||||||
|
|
||||||
-- AccessRepository.asset.checkPartnerAccess
|
-- AccessRepository.asset.checkPartnerAccess
|
||||||
select
|
select
|
||||||
@ -198,6 +199,15 @@ where
|
|||||||
"partners"."sharedById" in ($1)
|
"partners"."sharedById" in ($1)
|
||||||
and "partners"."sharedWithId" = $2
|
and "partners"."sharedWithId" = $2
|
||||||
|
|
||||||
|
-- AccessRepository.session.checkOwnerAccess
|
||||||
|
select
|
||||||
|
"sessions"."id"
|
||||||
|
from
|
||||||
|
"sessions"
|
||||||
|
where
|
||||||
|
"sessions"."id" in ($1)
|
||||||
|
and "sessions"."userId" = $2
|
||||||
|
|
||||||
-- AccessRepository.stack.checkOwnerAccess
|
-- AccessRepository.stack.checkOwnerAccess
|
||||||
select
|
select
|
||||||
"stacks"."id"
|
"stacks"."id"
|
||||||
|
@ -392,6 +392,11 @@ where
|
|||||||
order by
|
order by
|
||||||
"albums"."createdAt" desc
|
"albums"."createdAt" desc
|
||||||
|
|
||||||
|
-- AlbumRepository.removeAssetsFromAll
|
||||||
|
delete from "albums_assets_assets"
|
||||||
|
where
|
||||||
|
"albums_assets_assets"."assetsId" in ($1)
|
||||||
|
|
||||||
-- AlbumRepository.getAssetIds
|
-- AlbumRepository.getAssetIds
|
||||||
select
|
select
|
||||||
*
|
*
|
||||||
|
@ -432,3 +432,34 @@ where
|
|||||||
and "assets"."updatedAt" > $3
|
and "assets"."updatedAt" > $3
|
||||||
limit
|
limit
|
||||||
$4
|
$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"."entityId" desc,
|
||||||
"audit"."entityType" desc,
|
"audit"."entityType" desc,
|
||||||
"audit"."createdAt" 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
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
-- MemoryRepository.cleanup
|
|
||||||
delete from "memories"
|
|
||||||
where
|
|
||||||
"createdAt" < $1
|
|
||||||
and "isSaved" = $2
|
|
||||||
|
|
||||||
-- MemoryRepository.search
|
-- MemoryRepository.search
|
||||||
select
|
select
|
||||||
"memories".*,
|
"memories".*,
|
||||||
|
@ -16,19 +16,6 @@ where
|
|||||||
returning
|
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
|
-- MoveRepository.cleanMoveHistorySingle
|
||||||
delete from "move_history"
|
delete from "move_history"
|
||||||
where
|
where
|
||||||
|
@ -1,23 +1,5 @@
|
|||||||
-- NOTE: This file is auto generated by ./sql-generator
|
-- 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
|
-- NotificationRepository.search
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
|
@ -100,50 +100,6 @@ where
|
|||||||
"sharedWithId" = $1
|
"sharedWithId" = $1
|
||||||
and "sharedById" = $2
|
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
|
-- PartnerRepository.update
|
||||||
update "partners"
|
update "partners"
|
||||||
set
|
set
|
||||||
|
@ -7,34 +7,10 @@ set
|
|||||||
where
|
where
|
||||||
"asset_faces"."personId" = $2
|
"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
|
-- PersonRepository.delete
|
||||||
delete from "person"
|
delete from "person"
|
||||||
where
|
where
|
||||||
"person"."id" in $1
|
"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
|
|
||||||
|
|
||||||
-- PersonRepository.getAllWithoutFaces
|
-- PersonRepository.getAllWithoutFaces
|
||||||
select
|
select
|
||||||
@ -145,18 +121,24 @@ select
|
|||||||
"asset_faces"."imageHeight" as "oldHeight",
|
"asset_faces"."imageHeight" as "oldHeight",
|
||||||
"assets"."type",
|
"assets"."type",
|
||||||
"assets"."originalPath",
|
"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
|
from
|
||||||
"person"
|
"person"
|
||||||
inner join "asset_faces" on "asset_faces"."id" = "person"."faceAssetId"
|
inner join "asset_faces" on "asset_faces"."id" = "person"."faceAssetId"
|
||||||
inner join "assets" on "asset_faces"."assetId" = "assets"."id"
|
inner join "assets" on "asset_faces"."assetId" = "assets"."id"
|
||||||
left join "exif" on "exif"."assetId" = "assets"."id"
|
left join "exif" on "exif"."assetId" = "assets"."id"
|
||||||
left join "asset_files" on "asset_files"."assetId" = "assets"."id"
|
|
||||||
where
|
where
|
||||||
"person"."id" = $1
|
"person"."id" = $1
|
||||||
and "asset_faces"."deletedAt" is null
|
and "asset_faces"."deletedAt" is null
|
||||||
and "asset_files"."type" = $2
|
|
||||||
|
|
||||||
-- PersonRepository.reassignFace
|
-- PersonRepository.reassignFace
|
||||||
update "asset_faces"
|
update "asset_faces"
|
||||||
@ -222,21 +204,6 @@ where
|
|||||||
"person"."ownerId" = $3
|
"person"."ownerId" = $3
|
||||||
and "asset_faces"."deletedAt" is null
|
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
|
-- PersonRepository.getFacesByIds
|
||||||
select
|
select
|
||||||
"asset_faces".*,
|
"asset_faces".*,
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
-- NOTE: This file is auto generated by ./sql-generator
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
-- SessionRepository.search
|
-- SessionRepository.get
|
||||||
select
|
select
|
||||||
*
|
"id",
|
||||||
|
"expiresAt",
|
||||||
|
"pinExpiresAt"
|
||||||
from
|
from
|
||||||
"sessions"
|
"sessions"
|
||||||
where
|
where
|
||||||
"sessions"."updatedAt" <= $1
|
"id" = $1
|
||||||
|
|
||||||
-- SessionRepository.getByToken
|
-- SessionRepository.getByToken
|
||||||
select
|
select
|
||||||
"sessions"."id",
|
"sessions"."id",
|
||||||
"sessions"."updatedAt",
|
"sessions"."updatedAt",
|
||||||
|
"sessions"."pinExpiresAt",
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
to_json(obj)
|
to_json(obj)
|
||||||
@ -35,6 +38,10 @@ from
|
|||||||
"sessions"
|
"sessions"
|
||||||
where
|
where
|
||||||
"sessions"."token" = $1
|
"sessions"."token" = $1
|
||||||
|
and (
|
||||||
|
"sessions"."expiresAt" is null
|
||||||
|
or "sessions"."expiresAt" > $2
|
||||||
|
)
|
||||||
|
|
||||||
-- SessionRepository.getByUserId
|
-- SessionRepository.getByUserId
|
||||||
select
|
select
|
||||||
@ -45,6 +52,10 @@ from
|
|||||||
and "users"."deletedAt" is null
|
and "users"."deletedAt" is null
|
||||||
where
|
where
|
||||||
"sessions"."userId" = $1
|
"sessions"."userId" = $1
|
||||||
|
and (
|
||||||
|
"sessions"."expiresAt" is null
|
||||||
|
or "sessions"."expiresAt" > $2
|
||||||
|
)
|
||||||
order by
|
order by
|
||||||
"sessions"."updatedAt" desc,
|
"sessions"."updatedAt" desc,
|
||||||
"sessions"."createdAt" desc
|
"sessions"."createdAt" desc
|
||||||
@ -53,3 +64,10 @@ order by
|
|||||||
delete from "sessions"
|
delete from "sessions"
|
||||||
where
|
where
|
||||||
"id" = $1::uuid
|
"id" = $1::uuid
|
||||||
|
|
||||||
|
-- SessionRepository.lockAll
|
||||||
|
update "sessions"
|
||||||
|
set
|
||||||
|
"pinExpiresAt" = $1
|
||||||
|
where
|
||||||
|
"userId" = $2
|
||||||
|
@ -8,15 +8,6 @@ from
|
|||||||
where
|
where
|
||||||
"key" = $1
|
"key" = $1
|
||||||
|
|
||||||
-- SystemMetadataRepository.set
|
|
||||||
insert into
|
|
||||||
"system_metadata" ("key", "value")
|
|
||||||
values
|
|
||||||
($1, $2)
|
|
||||||
on conflict ("key") do update
|
|
||||||
set
|
|
||||||
"value" = $3
|
|
||||||
|
|
||||||
-- SystemMetadataRepository.delete
|
-- SystemMetadataRepository.delete
|
||||||
delete from "system_metadata"
|
delete from "system_metadata"
|
||||||
where
|
where
|
||||||
|
@ -58,7 +58,7 @@ from
|
|||||||
where
|
where
|
||||||
"userId" = $1
|
"userId" = $1
|
||||||
order by
|
order by
|
||||||
"value" asc
|
"value"
|
||||||
|
|
||||||
-- TagRepository.create
|
-- TagRepository.create
|
||||||
insert into
|
insert into
|
||||||
@ -94,6 +94,15 @@ where
|
|||||||
"tagsId" = $1
|
"tagsId" = $1
|
||||||
and "assetsId" in ($2)
|
and "assetsId" in ($2)
|
||||||
|
|
||||||
|
-- TagRepository.upsertAssetIds
|
||||||
|
insert into
|
||||||
|
"tag_asset" ("assetId", "tagsIds")
|
||||||
|
values
|
||||||
|
($1, $2)
|
||||||
|
on conflict do nothing
|
||||||
|
returning
|
||||||
|
*
|
||||||
|
|
||||||
-- TagRepository.replaceAssetTags
|
-- TagRepository.replaceAssetTags
|
||||||
begin
|
begin
|
||||||
delete from "tag_asset"
|
delete from "tag_asset"
|
||||||
@ -107,17 +116,3 @@ on conflict do nothing
|
|||||||
returning
|
returning
|
||||||
*
|
*
|
||||||
rollback
|
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"
|
"version_history"
|
||||||
order by
|
order by
|
||||||
"createdAt" desc
|
"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] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||||
@ChunkedSet({ paramIndex: 1 })
|
@ChunkedSet({ paramIndex: 1 })
|
||||||
async checkOwnerAccess(userId: string, assetIds: Set<string>) {
|
async checkOwnerAccess(userId: string, assetIds: Set<string>, hasElevatedPermission: boolean | undefined) {
|
||||||
if (assetIds.size === 0) {
|
if (assetIds.size === 0) {
|
||||||
return new Set<string>();
|
return new Set<string>();
|
||||||
}
|
}
|
||||||
@ -178,6 +178,7 @@ class AssetAccess {
|
|||||||
.select('assets.id')
|
.select('assets.id')
|
||||||
.where('assets.id', 'in', [...assetIds])
|
.where('assets.id', 'in', [...assetIds])
|
||||||
.where('assets.ownerId', '=', userId)
|
.where('assets.ownerId', '=', userId)
|
||||||
|
.$if(!hasElevatedPermission, (eb) => eb.where('assets.visibility', '!=', AssetVisibility.LOCKED))
|
||||||
.execute()
|
.execute()
|
||||||
.then((assets) => new Set(assets.map((asset) => asset.id)));
|
.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 {
|
class StackAccess {
|
||||||
constructor(private db: Kysely<DB>) {}
|
constructor(private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@ -455,6 +475,7 @@ export class AccessRepository {
|
|||||||
notification: NotificationAccess;
|
notification: NotificationAccess;
|
||||||
person: PersonAccess;
|
person: PersonAccess;
|
||||||
partner: PartnerAccess;
|
partner: PartnerAccess;
|
||||||
|
session: SessionAccess;
|
||||||
stack: StackAccess;
|
stack: StackAccess;
|
||||||
tag: TagAccess;
|
tag: TagAccess;
|
||||||
timeline: TimelineAccess;
|
timeline: TimelineAccess;
|
||||||
@ -468,6 +489,7 @@ export class AccessRepository {
|
|||||||
this.notification = new NotificationAccess(db);
|
this.notification = new NotificationAccess(db);
|
||||||
this.person = new PersonAccess(db);
|
this.person = new PersonAccess(db);
|
||||||
this.partner = new PartnerAccess(db);
|
this.partner = new PartnerAccess(db);
|
||||||
|
this.session = new SessionAccess(db);
|
||||||
this.stack = new StackAccess(db);
|
this.stack = new StackAccess(db);
|
||||||
this.tag = new TagAccess(db);
|
this.tag = new TagAccess(db);
|
||||||
this.timeline = new TimelineAccess(db);
|
this.timeline = new TimelineAccess(db);
|
||||||
|
@ -220,8 +220,10 @@ export class AlbumRepository {
|
|||||||
await this.db.deleteFrom('albums').where('ownerId', '=', userId).execute();
|
await this.db.deleteFrom('albums').where('ownerId', '=', userId).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAsset(assetId: string): Promise<void> {
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', '=', assetId).execute();
|
@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 })
|
@Chunked({ paramIndex: 1 })
|
||||||
|
@ -817,9 +817,7 @@ export class AssetRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING], [DummyValue.STRING]] })
|
||||||
params: [{ libraryId: DummyValue.UUID, importPaths: [DummyValue.STRING], exclusionPatterns: [DummyValue.STRING] }],
|
|
||||||
})
|
|
||||||
async detectOfflineExternalAssets(
|
async detectOfflineExternalAssets(
|
||||||
libraryId: string,
|
libraryId: string,
|
||||||
importPaths: string[],
|
importPaths: string[],
|
||||||
@ -846,9 +844,7 @@ export class AssetRepository {
|
|||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] })
|
||||||
params: [{ libraryId: DummyValue.UUID, paths: [DummyValue.STRING] }],
|
|
||||||
})
|
|
||||||
async filterNewExternalAssetPaths(libraryId: string, paths: string[]): Promise<string[]> {
|
async filterNewExternalAssetPaths(libraryId: string, paths: string[]): Promise<string[]> {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.selectFrom(unnest(paths).as('path'))
|
.selectFrom(unnest(paths).as('path'))
|
||||||
|
@ -38,7 +38,6 @@ export class AuditRepository {
|
|||||||
return records.map(({ entityId }) => entityId);
|
return records.map(({ entityId }) => entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.DATE] })
|
|
||||||
async removeBefore(before: Date): Promise<void> {
|
async removeBefore(before: Date): Promise<void> {
|
||||||
await this.db.deleteFrom('audit').where('createdAt', '<', before).execute();
|
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, '');
|
return randomBytes(bytes).toString('base64').replaceAll(/\W/g, '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ import { IBulkAsset } from 'src/types';
|
|||||||
export class MemoryRepository implements IBulkAsset {
|
export class MemoryRepository implements IBulkAsset {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
return this.db
|
return this.db
|
||||||
.deleteFrom('memories')
|
.deleteFrom('memories')
|
||||||
|
@ -37,7 +37,6 @@ export class MoveRepository {
|
|||||||
return this.db.deleteFrom('move_history').where('id', '=', id).returningAll().executeTakeFirstOrThrow();
|
return this.db.deleteFrom('move_history').where('id', '=', id).returningAll().executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql()
|
|
||||||
async cleanMoveHistory(): Promise<void> {
|
async cleanMoveHistory(): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.deleteFrom('move_history')
|
.deleteFrom('move_history')
|
||||||
@ -52,7 +51,7 @@ export class MoveRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql()
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async cleanMoveHistorySingle(assetId: string): Promise<void> {
|
async cleanMoveHistorySingle(assetId: string): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.deleteFrom('move_history')
|
.deleteFrom('move_history')
|
||||||
|
@ -9,7 +9,6 @@ import { NotificationSearchDto } from 'src/dtos/notification.dto';
|
|||||||
export class NotificationRepository {
|
export class NotificationRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
return this.db
|
return this.db
|
||||||
.deleteFrom('notifications')
|
.deleteFrom('notifications')
|
||||||
|
@ -47,7 +47,6 @@ export class PartnerRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ sharedWithId: DummyValue.UUID, sharedById: DummyValue.UUID }] })
|
|
||||||
create(values: Insertable<Partners>) {
|
create(values: Insertable<Partners>) {
|
||||||
return this.db
|
return this.db
|
||||||
.insertInto('partners')
|
.insertInto('partners')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
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 { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
||||||
@ -98,18 +98,15 @@ export class PersonRepository {
|
|||||||
return Number(result.numChangedRows ?? 0);
|
return Number(result.numChangedRows ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
|
|
||||||
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.updateTable('asset_faces')
|
.updateTable('asset_faces')
|
||||||
.set({ personId: null })
|
.set({ personId: null })
|
||||||
.where('asset_faces.sourceType', '=', sourceType)
|
.where('asset_faces.sourceType', '=', sourceType)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
await this.vacuum({ reindexVectors: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
async delete(ids: string[]): Promise<void> {
|
async delete(ids: string[]): Promise<void> {
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
return;
|
return;
|
||||||
@ -118,11 +115,8 @@ export class PersonRepository {
|
|||||||
await this.db.deleteFrom('person').where('person.id', 'in', ids).execute();
|
await this.db.deleteFrom('person').where('person.id', 'in', ids).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
|
|
||||||
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
||||||
await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute();
|
await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute();
|
||||||
|
|
||||||
await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllFaces(options: GetAllFacesOptions = {}) {
|
getAllFaces(options: GetAllFacesOptions = {}) {
|
||||||
@ -265,7 +259,6 @@ export class PersonRepository {
|
|||||||
.innerJoin('asset_faces', 'asset_faces.id', 'person.faceAssetId')
|
.innerJoin('asset_faces', 'asset_faces.id', 'person.faceAssetId')
|
||||||
.innerJoin('assets', 'asset_faces.assetId', 'assets.id')
|
.innerJoin('assets', 'asset_faces.assetId', 'assets.id')
|
||||||
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
.leftJoin('exif', 'exif.assetId', 'assets.id')
|
||||||
.leftJoin('asset_files', 'asset_files.assetId', 'assets.id')
|
|
||||||
.select([
|
.select([
|
||||||
'person.ownerId',
|
'person.ownerId',
|
||||||
'asset_faces.boundingBoxX1 as x1',
|
'asset_faces.boundingBoxX1 as x1',
|
||||||
@ -276,13 +269,18 @@ export class PersonRepository {
|
|||||||
'asset_faces.imageHeight as oldHeight',
|
'asset_faces.imageHeight as oldHeight',
|
||||||
'assets.type',
|
'assets.type',
|
||||||
'assets.originalPath',
|
'assets.originalPath',
|
||||||
'asset_files.path as previewPath',
|
|
||||||
'exif.orientation as exifOrientation',
|
'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('person.id', '=', id)
|
||||||
.where('asset_faces.deletedAt', 'is', null)
|
.where('asset_faces.deletedAt', 'is', null)
|
||||||
.where('asset_files.type', '=', AssetFileType.PREVIEW)
|
|
||||||
.$narrowType<{ exifImageWidth: NotNull; exifImageHeight: NotNull }>()
|
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,7 +398,6 @@ export class PersonRepository {
|
|||||||
return results.map(({ id }) => id);
|
return results.map(({ id }) => id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[], [], [{ faceId: DummyValue.UUID, embedding: DummyValue.VECTOR }]] })
|
|
||||||
async refreshFaces(
|
async refreshFaces(
|
||||||
facesToAdd: (Insertable<AssetFaces> & { assetId: string })[],
|
facesToAdd: (Insertable<AssetFaces> & { assetId: string })[],
|
||||||
faceIdsToRemove: 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();
|
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`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db);
|
||||||
await sql`REINDEX TABLE asset_faces`.execute(this.db);
|
await sql`REINDEX TABLE asset_faces`.execute(this.db);
|
||||||
await sql`REINDEX TABLE person`.execute(this.db);
|
await sql`REINDEX TABLE person`.execute(this.db);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns } from 'src/database';
|
import { columns } from 'src/database';
|
||||||
import { DB, Sessions } from 'src/db';
|
import { DB, Sessions } from 'src/db';
|
||||||
@ -13,13 +14,26 @@ export type SessionSearchOptions = { updatedBefore: Date };
|
|||||||
export class SessionRepository {
|
export class SessionRepository {
|
||||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] })
|
cleanup() {
|
||||||
search(options: SessionSearchOptions) {
|
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
|
return this.db
|
||||||
.selectFrom('sessions')
|
.selectFrom('sessions')
|
||||||
.selectAll()
|
.select(['id', 'expiresAt', 'pinExpiresAt'])
|
||||||
.where('sessions.updatedAt', '<=', options.updatedBefore)
|
.where('id', '=', id)
|
||||||
.execute();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.STRING] })
|
||||||
@ -37,6 +51,9 @@ export class SessionRepository {
|
|||||||
).as('user'),
|
).as('user'),
|
||||||
])
|
])
|
||||||
.where('sessions.token', '=', token)
|
.where('sessions.token', '=', token)
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
|
||||||
|
)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +64,9 @@ export class SessionRepository {
|
|||||||
.innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null))
|
.innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null))
|
||||||
.selectAll('sessions')
|
.selectAll('sessions')
|
||||||
.where('sessions.userId', '=', userId)
|
.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.updatedAt', 'desc')
|
||||||
.orderBy('sessions.createdAt', 'desc')
|
.orderBy('sessions.createdAt', 'desc')
|
||||||
.execute();
|
.execute();
|
||||||
@ -69,4 +89,9 @@ export class SessionRepository {
|
|||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
|
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];
|
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> {
|
async set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
.insertInto('system_metadata')
|
.insertInto('system_metadata')
|
||||||
|
@ -68,7 +68,7 @@ export class TagRepository {
|
|||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getAll(userId: string) {
|
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 }] })
|
@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();
|
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()
|
@Chunked()
|
||||||
upsertAssetIds(items: Insertable<TagAsset>[]) {
|
upsertAssetIds(items: Insertable<TagAsset>[]) {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@ -160,7 +160,6 @@ export class TagRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql()
|
|
||||||
async deleteEmptyTags() {
|
async deleteEmptyTags() {
|
||||||
// TODO rewrite as a single statement
|
// TODO rewrite as a single statement
|
||||||
await this.db.transaction().execute(async (tx) => {
|
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();
|
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ version: 'v1.123.0' }] })
|
|
||||||
create(version: Insertable<VersionHistory>) {
|
create(version: Insertable<VersionHistory>) {
|
||||||
return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow();
|
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()
|
@UpdateDateColumn()
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
|
expiresAt!: Date | null;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => SessionTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', nullable: true })
|
||||||
|
parentId!: string | null;
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
deviceType!: string;
|
deviceType!: string;
|
||||||
|
|
||||||
@ -36,4 +42,7 @@ export class SessionTable {
|
|||||||
|
|
||||||
@UpdateIdColumn({ indexName: 'IDX_sessions_update_id' })
|
@UpdateIdColumn({ indexName: 'IDX_sessions_update_id' })
|
||||||
updateId!: string;
|
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.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', {
|
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
|
||||||
id: albumStub.empty.id,
|
id: albumStub.empty.id,
|
||||||
userId: 'user-id',
|
userId: 'user-id',
|
||||||
@ -207,6 +207,7 @@ describe(AlbumService.name, () => {
|
|||||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||||
authStub.admin.user.id,
|
authStub.admin.user.id,
|
||||||
new Set(['asset-1', 'asset-2']),
|
new Set(['asset-1', 'asset-2']),
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -688,7 +689,11 @@ describe(AlbumService.name, () => {
|
|||||||
{ success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION },
|
{ 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']));
|
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 apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.ALL] });
|
||||||
const key = 'super-secret';
|
const key = 'super-secret';
|
||||||
|
|
||||||
mocks.crypto.newPassword.mockReturnValue(key);
|
mocks.crypto.randomBytesAsText.mockReturnValue(key);
|
||||||
mocks.apiKey.create.mockResolvedValue(apiKey);
|
mocks.apiKey.create.mockResolvedValue(apiKey);
|
||||||
|
|
||||||
await sut.create(auth, { name: apiKey.name, permissions: apiKey.permissions });
|
await sut.create(auth, { name: apiKey.name, permissions: apiKey.permissions });
|
||||||
@ -29,7 +29,7 @@ describe(ApiKeyService.name, () => {
|
|||||||
permissions: apiKey.permissions,
|
permissions: apiKey.permissions,
|
||||||
userId: apiKey.userId,
|
userId: apiKey.userId,
|
||||||
});
|
});
|
||||||
expect(mocks.crypto.newPassword).toHaveBeenCalled();
|
expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled();
|
||||||
expect(mocks.crypto.hashSha256).toHaveBeenCalled();
|
expect(mocks.crypto.hashSha256).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ describe(ApiKeyService.name, () => {
|
|||||||
const apiKey = factory.apiKey({ userId: auth.user.id });
|
const apiKey = factory.apiKey({ userId: auth.user.id });
|
||||||
const key = 'super-secret';
|
const key = 'super-secret';
|
||||||
|
|
||||||
mocks.crypto.newPassword.mockReturnValue(key);
|
mocks.crypto.randomBytesAsText.mockReturnValue(key);
|
||||||
mocks.apiKey.create.mockResolvedValue(apiKey);
|
mocks.apiKey.create.mockResolvedValue(apiKey);
|
||||||
|
|
||||||
await sut.create(auth, { permissions: [Permission.ALL] });
|
await sut.create(auth, { permissions: [Permission.ALL] });
|
||||||
@ -49,7 +49,7 @@ describe(ApiKeyService.name, () => {
|
|||||||
permissions: [Permission.ALL],
|
permissions: [Permission.ALL],
|
||||||
userId: auth.user.id,
|
userId: auth.user.id,
|
||||||
});
|
});
|
||||||
expect(mocks.crypto.newPassword).toHaveBeenCalled();
|
expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled();
|
||||||
expect(mocks.crypto.hashSha256).toHaveBeenCalled();
|
expect(mocks.crypto.hashSha256).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,20 +9,21 @@ import { isGranted } from 'src/utils/access';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiKeyService extends BaseService {
|
export class ApiKeyService extends BaseService {
|
||||||
async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
|
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 })) {
|
if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) {
|
||||||
throw new BadRequestException('Cannot grant permissions you do not have');
|
throw new BadRequestException('Cannot grant permissions you do not have');
|
||||||
}
|
}
|
||||||
|
|
||||||
const entity = await this.apiKeyRepository.create({
|
const entity = await this.apiKeyRepository.create({
|
||||||
key: this.cryptoRepository.hashSha256(secret),
|
key: tokenHashed,
|
||||||
name: dto.name || 'API Key',
|
name: dto.name || 'API Key',
|
||||||
userId: auth.user.id,
|
userId: auth.user.id,
|
||||||
permissions: dto.permissions,
|
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> {
|
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 () => {
|
it('should require the asset.download permission', async () => {
|
||||||
await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException);
|
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.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1']));
|
||||||
expect(mocks.access.asset.checkPartnerAccess).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 () => {
|
it('should require asset.view permissions', async () => {
|
||||||
await expect(sut.viewThumbnail(authStub.admin, 'id', {})).rejects.toBeInstanceOf(BadRequestException);
|
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.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||||
expect(mocks.access.asset.checkPartnerAccess).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 () => {
|
it('should require asset.view permissions', async () => {
|
||||||
await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
|
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.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||||
expect(mocks.access.asset.checkPartnerAccess).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(
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||||
authStub.admin.user.id,
|
authStub.admin.user.id,
|
||||||
new Set([assetStub.image.id]),
|
new Set([assetStub.image.id]),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
mapStats,
|
mapStats,
|
||||||
} from 'src/dtos/asset.dto';
|
} from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.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 { BaseService } from 'src/services/base.service';
|
||||||
import { ISidecarWriteJob, JobItem, JobOf } from 'src/types';
|
import { ISidecarWriteJob, JobItem, JobOf } from 'src/types';
|
||||||
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
||||||
@ -125,6 +125,10 @@ export class AssetService extends BaseService {
|
|||||||
options.rating !== undefined
|
options.rating !== undefined
|
||||||
) {
|
) {
|
||||||
await this.assetRepository.updateAll(ids, options);
|
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,
|
id: session.id,
|
||||||
updatedAt: session.updatedAt,
|
updatedAt: session.updatedAt,
|
||||||
user: factory.authUser(),
|
user: factory.authUser(),
|
||||||
|
pinExpiresAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
||||||
@ -265,7 +266,7 @@ describe(AuthService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
user: sessionWithToken.user,
|
user: sessionWithToken.user,
|
||||||
session: { id: session.id },
|
session: { id: session.id, hasElevatedPermission: false },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -376,6 +377,7 @@ describe(AuthService.name, () => {
|
|||||||
id: session.id,
|
id: session.id,
|
||||||
updatedAt: session.updatedAt,
|
updatedAt: session.updatedAt,
|
||||||
user: factory.authUser(),
|
user: factory.authUser(),
|
||||||
|
pinExpiresAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
||||||
@ -388,7 +390,7 @@ describe(AuthService.name, () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual({
|
||||||
user: sessionWithToken.user,
|
user: sessionWithToken.user,
|
||||||
session: { id: session.id },
|
session: { id: session.id, hasElevatedPermission: false },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -398,6 +400,7 @@ describe(AuthService.name, () => {
|
|||||||
id: session.id,
|
id: session.id,
|
||||||
updatedAt: session.updatedAt,
|
updatedAt: session.updatedAt,
|
||||||
user: factory.authUser(),
|
user: factory.authUser(),
|
||||||
|
pinExpiresAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
||||||
@ -417,6 +420,7 @@ describe(AuthService.name, () => {
|
|||||||
id: session.id,
|
id: session.id,
|
||||||
updatedAt: session.updatedAt,
|
updatedAt: session.updatedAt,
|
||||||
user: factory.authUser(),
|
user: factory.authUser(),
|
||||||
|
pinExpiresAt: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
mocks.session.getByToken.mockResolvedValue(sessionWithToken);
|
||||||
@ -916,13 +920,17 @@ describe(AuthService.name, () => {
|
|||||||
|
|
||||||
describe('resetPinCode', () => {
|
describe('resetPinCode', () => {
|
||||||
it('should reset the PIN code', async () => {
|
it('should reset the PIN code', async () => {
|
||||||
|
const currentSession = factory.session();
|
||||||
const user = factory.userAdmin();
|
const user = factory.userAdmin();
|
||||||
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||||
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
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' });
|
await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' });
|
||||||
|
|
||||||
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
|
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 () => {
|
it('should throw if the PIN code does not match', async () => {
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
PinCodeChangeDto,
|
PinCodeChangeDto,
|
||||||
PinCodeResetDto,
|
PinCodeResetDto,
|
||||||
PinCodeSetupDto,
|
PinCodeSetupDto,
|
||||||
|
SessionUnlockDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
mapLoginResponse,
|
mapLoginResponse,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
@ -123,20 +124,21 @@ export class AuthService extends BaseService {
|
|||||||
|
|
||||||
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
|
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
|
||||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
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.userRepository.update(auth.user.id, { pinCode: null });
|
||||||
|
await this.sessionRepository.lockAll(auth.user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
|
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
|
||||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
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);
|
const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
|
||||||
await this.userRepository.update(auth.user.id, { pinCode: hashed });
|
await this.userRepository.update(auth.user.id, { pinCode: hashed });
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetPinChecks(
|
private validatePinCode(
|
||||||
user: { pinCode: string | null; password: string | null },
|
user: { pinCode: string | null; password: string | null },
|
||||||
dto: { pinCode?: string; password?: string },
|
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() });
|
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 {
|
return {
|
||||||
user: session.user,
|
user: session.user,
|
||||||
session: {
|
session: {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
|
hasElevatedPermission,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -455,18 +472,39 @@ export class AuthService extends BaseService {
|
|||||||
throw new UnauthorizedException('Invalid user token');
|
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) {
|
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
|
||||||
const key = this.cryptoRepository.newPassword(32);
|
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||||
const token = this.cryptoRepository.hashSha256(key);
|
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
||||||
|
|
||||||
await this.sessionRepository.create({
|
await this.sessionRepository.create({
|
||||||
token,
|
token: tokenHashed,
|
||||||
deviceOS: loginDetails.deviceOS,
|
deviceOS: loginDetails.deviceOS,
|
||||||
deviceType: loginDetails.deviceType,
|
deviceType: loginDetails.deviceType,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapLoginResponse(user, key);
|
return mapLoginResponse(user, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
||||||
@ -490,9 +528,14 @@ export class AuthService extends BaseService {
|
|||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = auth.session ? await this.sessionRepository.get(auth.session.id) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pinCode: !!user.pinCode,
|
pinCode: !!user.pinCode,
|
||||||
password: !!user.password,
|
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 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);
|
const hashedPassword = await this.cryptoRepository.hashBcrypt(password, SALT_ROUNDS);
|
||||||
|
|
||||||
await this.userRepository.update(admin.id, { password: hashedPassword });
|
await this.userRepository.update(admin.id, { password: hashedPassword });
|
||||||
|
@ -1310,7 +1310,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
|
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 () => {
|
it('should handle not finding a match', async () => {
|
||||||
@ -1331,7 +1331,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
|
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
|
||||||
);
|
);
|
||||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
expect(mocks.album.removeAssetsFromAll).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should link photo and video', async () => {
|
it('should link photo and video', async () => {
|
||||||
@ -1356,7 +1356,7 @@ describe(MetadataService.name, () => {
|
|||||||
id: assetStub.livePhotoMotionAsset.id,
|
id: assetStub.livePhotoMotionAsset.id,
|
||||||
visibility: AssetVisibility.HIDDEN,
|
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 () => {
|
it('should notify clients on live photo link', async () => {
|
||||||
|
@ -158,7 +158,7 @@ export class MetadataService extends BaseService {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }),
|
this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }),
|
||||||
this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.HIDDEN }),
|
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 });
|
await this.eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
|
||||||
|
@ -459,6 +459,7 @@ describe(PersonService.name, () => {
|
|||||||
await sut.handleQueueDetectFaces({ force: false });
|
await sut.handleQueueDetectFaces({ force: false });
|
||||||
|
|
||||||
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(false);
|
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(false);
|
||||||
|
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.FACE_DETECTION,
|
name: JobName.FACE_DETECTION,
|
||||||
@ -475,6 +476,7 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
|
expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
|
||||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]);
|
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.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath);
|
||||||
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true);
|
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true);
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
@ -492,6 +494,7 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.person.delete).not.toHaveBeenCalled();
|
expect(mocks.person.delete).not.toHaveBeenCalled();
|
||||||
expect(mocks.person.deleteFaces).not.toHaveBeenCalled();
|
expect(mocks.person.deleteFaces).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||||
expect(mocks.storage.unlink).not.toHaveBeenCalled();
|
expect(mocks.storage.unlink).not.toHaveBeenCalled();
|
||||||
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(undefined);
|
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(undefined);
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
@ -521,6 +524,7 @@ describe(PersonService.name, () => {
|
|||||||
]);
|
]);
|
||||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
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, {
|
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
|
||||||
lastRun: expect.any(String),
|
lastRun: expect.any(String),
|
||||||
});
|
});
|
||||||
|
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should queue all assets', async () => {
|
it('should queue all assets', async () => {
|
||||||
@ -611,6 +616,7 @@ describe(PersonService.name, () => {
|
|||||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
|
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
|
||||||
lastRun: expect.any(String),
|
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 () => {
|
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.getAllWithoutFaces.mockResolvedValue([]);
|
||||||
mocks.person.unassignFaces.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.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
|
||||||
expect(mocks.person.getLatestFaceDate).toHaveBeenCalledOnce();
|
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([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.FACIAL_RECOGNITION,
|
name: JobName.FACIAL_RECOGNITION,
|
||||||
@ -643,6 +652,7 @@ describe(PersonService.name, () => {
|
|||||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
|
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
|
||||||
lastRun: expect.any(String),
|
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 () => {
|
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.person.getAllFaces).not.toHaveBeenCalled();
|
||||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||||
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
expect(mocks.systemMetadata.set).not.toHaveBeenCalled();
|
||||||
|
expect(mocks.person.vacuum).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete existing people if forced', async () => {
|
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.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
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) {
|
if (force) {
|
||||||
await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
await this.personRepository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
||||||
await this.handlePersonCleanup();
|
await this.handlePersonCleanup();
|
||||||
|
await this.personRepository.vacuum({ reindexVectors: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
let jobs: JobItem[] = [];
|
let jobs: JobItem[] = [];
|
||||||
@ -409,6 +410,7 @@ export class PersonService extends BaseService {
|
|||||||
if (force) {
|
if (force) {
|
||||||
await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
await this.personRepository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
||||||
await this.handlePersonCleanup();
|
await this.handlePersonCleanup();
|
||||||
|
await this.personRepository.vacuum({ reindexVectors: false });
|
||||||
} else if (waiting) {
|
} else if (waiting) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`,
|
`Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`,
|
||||||
|
@ -17,29 +17,9 @@ describe('SessionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleCleanup', () => {
|
describe('handleCleanup', () => {
|
||||||
it('should return skipped if nothing is to be deleted', async () => {
|
it('should clean sessions', async () => {
|
||||||
mocks.session.search.mockResolvedValue([]);
|
mocks.session.cleanup.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();
|
|
||||||
|
|
||||||
await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SUCCESS);
|
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 { DateTime } from 'luxon';
|
||||||
import { OnJob } from 'src/decorators';
|
import { OnJob } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
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 { JobName, JobStatus, Permission, QueueName } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
@ -10,16 +10,8 @@ import { BaseService } from 'src/services/base.service';
|
|||||||
export class SessionService extends BaseService {
|
export class SessionService extends BaseService {
|
||||||
@OnJob({ name: JobName.CLEAN_OLD_SESSION_TOKENS, queue: QueueName.BACKGROUND_TASK })
|
@OnJob({ name: JobName.CLEAN_OLD_SESSION_TOKENS, queue: QueueName.BACKGROUND_TASK })
|
||||||
async handleCleanup(): Promise<JobStatus> {
|
async handleCleanup(): Promise<JobStatus> {
|
||||||
const sessions = await this.sessionRepository.search({
|
const sessions = await this.sessionRepository.cleanup();
|
||||||
updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sessions.length === 0) {
|
|
||||||
return JobStatus.SKIPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
await this.sessionRepository.delete(session.id);
|
|
||||||
this.logger.verbose(`Deleted expired session token: ${session.deviceOS}/${session.deviceType}`);
|
this.logger.verbose(`Deleted expired session token: ${session.deviceOS}/${session.deviceType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,6 +20,25 @@ export class SessionService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
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[]> {
|
async getAll(auth: AuthDto): Promise<SessionResponseDto[]> {
|
||||||
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
||||||
return sessions.map((session) => mapSession(session, auth.session?.id));
|
return sessions.map((session) => mapSession(session, auth.session?.id));
|
||||||
@ -38,6 +49,11 @@ export class SessionService extends BaseService {
|
|||||||
await this.sessionRepository.delete(id);
|
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> {
|
async deleteAll(auth: AuthDto): Promise<void> {
|
||||||
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
|
@ -156,6 +156,7 @@ describe(SharedLinkService.name, () => {
|
|||||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||||
authStub.admin.user.id,
|
authStub.admin.user.id,
|
||||||
new Set([assetStub.image.id]),
|
new Set([assetStub.image.id]),
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||||
type: SharedLinkType.INDIVIDUAL,
|
type: SharedLinkType.INDIVIDUAL,
|
||||||
@ -186,6 +187,7 @@ describe(SharedLinkService.name, () => {
|
|||||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||||
authStub.admin.user.id,
|
authStub.admin.user.id,
|
||||||
new Set([assetStub.image.id]),
|
new Set([assetStub.image.id]),
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
expect(mocks.sharedLink.create).toHaveBeenCalledWith({
|
||||||
type: SharedLinkType.INDIVIDUAL,
|
type: SharedLinkType.INDIVIDUAL,
|
||||||
|
@ -81,7 +81,7 @@ const checkSharedLinkAccess = async (
|
|||||||
|
|
||||||
case Permission.ASSET_SHARE: {
|
case Permission.ASSET_SHARE: {
|
||||||
// TODO: fix this to not use sharedLink.userId for access control
|
// 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: {
|
case Permission.ALBUM_READ: {
|
||||||
@ -119,38 +119,38 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
case Permission.ASSET_READ: {
|
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 isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||||
return setUnion(isOwner, isAlbum, isPartner);
|
return setUnion(isOwner, isAlbum, isPartner);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.ASSET_SHARE: {
|
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));
|
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner));
|
||||||
return setUnion(isOwner, isPartner);
|
return setUnion(isOwner, isPartner);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.ASSET_VIEW: {
|
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 isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||||
return setUnion(isOwner, isAlbum, isPartner);
|
return setUnion(isOwner, isAlbum, isPartner);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.ASSET_DOWNLOAD: {
|
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 isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner));
|
||||||
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum));
|
||||||
return setUnion(isOwner, isAlbum, isPartner);
|
return setUnion(isOwner, isAlbum, isPartner);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Permission.ASSET_UPDATE: {
|
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: {
|
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: {
|
case Permission.ALBUM_READ: {
|
||||||
@ -280,6 +280,13 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
return await access.partner.checkUpdateAccess(auth.user.id, ids);
|
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: {
|
case Permission.STACK_READ: {
|
||||||
return access.stack.checkOwnerAccess(auth.user.id, ids);
|
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';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
|
||||||
const authUser = {
|
const authUser = {
|
||||||
@ -26,7 +26,7 @@ export const authStub = {
|
|||||||
user: authUser.user1,
|
user: authUser.user1,
|
||||||
session: {
|
session: {
|
||||||
id: 'token-id',
|
id: 'token-id',
|
||||||
} as Session,
|
} as AuthSession,
|
||||||
}),
|
}),
|
||||||
user2: Object.freeze<AuthDto>({
|
user2: Object.freeze<AuthDto>({
|
||||||
user: {
|
user: {
|
||||||
@ -39,7 +39,7 @@ export const authStub = {
|
|||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
id: 'token-id',
|
id: 'token-id',
|
||||||
} as Session,
|
} as AuthSession,
|
||||||
}),
|
}),
|
||||||
adminSharedLink: Object.freeze({
|
adminSharedLink: Object.freeze({
|
||||||
user: authUser.admin,
|
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,
|
isTrashed: false,
|
||||||
libraryId: 'library-id',
|
libraryId: 'library-id',
|
||||||
hasMetadata: true,
|
hasMetadata: true,
|
||||||
|
visibility: AssetVisibility.TIMELINE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const assetResponseWithoutMetadata = {
|
const assetResponseWithoutMetadata = {
|
||||||
|
@ -50,6 +50,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
|||||||
checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
session: {
|
||||||
|
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
|
},
|
||||||
|
|
||||||
stack: {
|
stack: {
|
||||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
},
|
},
|
||||||
|
@ -12,6 +12,6 @@ export const newCryptoRepositoryMock = (): Mocked<RepositoryInterface<CryptoRepo
|
|||||||
verifySha256: vitest.fn().mockImplementation(() => true),
|
verifySha256: vitest.fn().mockImplementation(() => true),
|
||||||
hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)),
|
hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)),
|
||||||
hashFile: vitest.fn().mockImplementation((input) => `${input} (file-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(),
|
createAssetFace: vitest.fn(),
|
||||||
deleteAssetFace: vitest.fn(),
|
deleteAssetFace: vitest.fn(),
|
||||||
softDeleteAssetFaces: vitest.fn(),
|
softDeleteAssetFaces: vitest.fn(),
|
||||||
|
vacuum: vitest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -58,7 +58,7 @@ const authFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
auth.session = { id: session.id };
|
auth.session = { id: session.id, hasElevatedPermission: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sharedLink) {
|
if (sharedLink) {
|
||||||
@ -126,7 +126,10 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
|
|||||||
deviceOS: 'android',
|
deviceOS: 'android',
|
||||||
deviceType: 'mobile',
|
deviceType: 'mobile',
|
||||||
token: 'abc123',
|
token: 'abc123',
|
||||||
|
parentId: null,
|
||||||
|
expiresAt: null,
|
||||||
userId: newUuid(),
|
userId: newUuid(),
|
||||||
|
pinExpiresAt: newDate(),
|
||||||
...session,
|
...session,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@ -11,7 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@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",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.11.5",
|
"@photo-sphere-viewer/core": "^5.11.5",
|
||||||
@ -1337,9 +1337,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@immich/ui": {
|
"node_modules/@immich/ui": {
|
||||||
"version": "0.20.0",
|
"version": "0.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.21.1.tgz",
|
||||||
"integrity": "sha512-euK3N0AhQLB28qFteorRKyDUdet3UpA9MEAd8eBLbTtTFZKvZismBGa4J7pHbQrSkuOlbmJD5LJuM575q8zigQ==",
|
"integrity": "sha512-ofDbLMYgM3Bnrv1nCbyPV5Gw9PdWvyhTAJPtojw4C3r2m7CbRW1kJDHt5M79n6xAVgjMOFyre1lOE5cwSSvRQA==",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
|
@ -18,7 +18,8 @@
|
|||||||
"lint:p": "eslint-p . --max-warnings 0 --concurrency=4",
|
"lint:p": "eslint-p . --max-warnings 0 --concurrency=4",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "npm run lint -- --fix",
|
||||||
"format": "prettier --check .",
|
"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": "vitest --run",
|
||||||
"test:cov": "vitest --coverage",
|
"test:cov": "vitest --coverage",
|
||||||
"test:watch": "vitest dev",
|
"test:watch": "vitest dev",
|
||||||
@ -27,7 +28,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@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",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.11.5",
|
"@photo-sphere-viewer/core": "^5.11.5",
|
||||||
|
@ -21,34 +21,6 @@
|
|||||||
--immich-dark-success: 56 142 60;
|
--immich-dark-success: 56 142 60;
|
||||||
--immich-dark-warning: 245 124 0;
|
--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 {
|
@font-face {
|
||||||
|
@ -13,6 +13,8 @@ type ActionMap = {
|
|||||||
[AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
|
[AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
|
||||||
[AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
|
[AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
|
||||||
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
|
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
|
||||||
|
[AssetAction.SET_VISIBILITY_LOCKED]: { asset: AssetResponseDto };
|
||||||
|
[AssetAction.SET_VISIBILITY_TIMELINE]: { asset: AssetResponseDto };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action = {
|
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 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 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 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 ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
|
||||||
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-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';
|
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
|
||||||
@ -27,6 +28,7 @@
|
|||||||
import {
|
import {
|
||||||
AssetJobName,
|
AssetJobName,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
|
Visibility,
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
type PersonResponseDto,
|
type PersonResponseDto,
|
||||||
@ -91,6 +93,7 @@
|
|||||||
const sharedLink = getSharedLink();
|
const sharedLink = getSharedLink();
|
||||||
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
||||||
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
||||||
|
let isLocked = $derived(asset.visibility === Visibility.Locked);
|
||||||
|
|
||||||
// $: showEditorButton =
|
// $: showEditorButton =
|
||||||
// isOwner &&
|
// isOwner &&
|
||||||
@ -112,7 +115,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 overflow-x-auto text-white" data-testid="asset-viewer-navbar-actions">
|
<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} />
|
<ShareAction {asset} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if asset.isOffline}
|
{#if asset.isOffline}
|
||||||
@ -159,17 +162,20 @@
|
|||||||
<DeleteAction {asset} {onAction} {preAction} />
|
<DeleteAction {asset} {onAction} {preAction} />
|
||||||
|
|
||||||
<ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}>
|
<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} />
|
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if showDownloadButton}
|
{#if showDownloadButton}
|
||||||
<DownloadAction {asset} menuItem />
|
<DownloadAction {asset} menuItem />
|
||||||
{/if}
|
{/if}
|
||||||
{#if asset.isTrashed}
|
|
||||||
<RestoreAction {asset} {onAction} />
|
{#if !isLocked}
|
||||||
{:else}
|
{#if asset.isTrashed}
|
||||||
<AddToAlbumAction {asset} {onAction} />
|
<RestoreAction {asset} {onAction} />
|
||||||
<AddToAlbumAction {asset} {onAction} shared />
|
{:else}
|
||||||
|
<AddToAlbumAction {asset} {onAction} />
|
||||||
|
<AddToAlbumAction {asset} {onAction} shared />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
@ -183,21 +189,28 @@
|
|||||||
{#if person}
|
{#if person}
|
||||||
<SetFeaturedPhotoAction {asset} {person} />
|
<SetFeaturedPhotoAction {asset} {person} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if asset.type === AssetTypeEnum.Image}
|
{#if asset.type === AssetTypeEnum.Image && !isLocked}
|
||||||
<SetProfilePictureAction {asset} />
|
<SetProfilePictureAction {asset} />
|
||||||
{/if}
|
{/if}
|
||||||
<ArchiveAction {asset} {onAction} {preAction} />
|
|
||||||
<MenuOption
|
{#if !isLocked}
|
||||||
icon={mdiUpload}
|
<ArchiveAction {asset} {onAction} {preAction} />
|
||||||
onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
|
|
||||||
text={$t('replace_with_upload')}
|
|
||||||
/>
|
|
||||||
{#if !asset.isArchived && !asset.isTrashed}
|
|
||||||
<MenuOption
|
<MenuOption
|
||||||
icon={mdiImageSearch}
|
icon={mdiUpload}
|
||||||
onClick={() => goto(`${AppRoute.PHOTOS}?at=${stack?.primaryAssetId ?? asset.id}`)}
|
onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
|
||||||
text={$t('view_in_timeline')}
|
text={$t('replace_with_upload')}
|
||||||
/>
|
/>
|
||||||
|
{#if !asset.isArchived && !asset.isTrashed}
|
||||||
|
<MenuOption
|
||||||
|
icon={mdiImageSearch}
|
||||||
|
onClick={() => goto(`${AppRoute.PHOTOS}?at=${stack?.primaryAssetId ?? asset.id}`)}
|
||||||
|
text={$t('view_in_timeline')}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !asset.isTrashed}
|
||||||
|
<SetVisibilityAction {asset} {onAction} {preAction} />
|
||||||
{/if}
|
{/if}
|
||||||
<hr />
|
<hr />
|
||||||
<MenuOption
|
<MenuOption
|
||||||
|
@ -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">
|
<script lang="ts">
|
||||||
import { getTabbable } from '$lib/utils/focus-util';
|
import { getTabbable } from '$lib/utils/focus-util';
|
||||||
|
import { Button } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import Button from './button.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@ -58,8 +58,7 @@
|
|||||||
|
|
||||||
<div class="absolute top-2 start-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
|
<div class="absolute top-2 start-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="small"
|
||||||
rounded="none"
|
|
||||||
onclick={moveFocus}
|
onclick={moveFocus}
|
||||||
class={getBreakpoint()}
|
class={getBreakpoint()}
|
||||||
onfocus={() => (isFocused = true)}
|
onfocus={() => (isFocused = true)}
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
transition:fly={{ x: 360, duration: 100, easing: linear }}
|
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">
|
<div class="flex place-items-center justify-between gap-2">
|
||||||
{#if !searchFaces}
|
{#if !searchFaces}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<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 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 { t } from 'svelte-i18n';
|
||||||
|
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
person: PersonResponseDto;
|
person: PersonResponseDto;
|
||||||
@ -44,6 +44,6 @@
|
|||||||
inputClass="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
inputClass="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
||||||
bind:showLoadingSpinner={isSearchingPeople}
|
bind:showLoadingSpinner={isSearchingPeople}
|
||||||
/>
|
/>
|
||||||
<Button size="sm" type="submit">{$t('done')}</Button>
|
<Button size="small" shape="round" type="submit">{$t('done')}</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { shortcut } from '$lib/actions/shortcut';
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
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 PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
|
||||||
import {
|
import {
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@ -13,6 +11,7 @@
|
|||||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { updatePeople, type PersonResponseDto } from '@immich/sdk';
|
import { updatePeople, type PersonResponseDto } from '@immich/sdk';
|
||||||
|
import { Button } from '@immich/ui';
|
||||||
import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
|
import { mdiClose, mdiEye, mdiEyeOff, mdiEyeSettings, mdiRestart } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
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={$t('reset_people_visibility')} icon={mdiRestart} onclick={handleResetVisibility} />
|
||||||
<CircleIconButton title={toggleButton.label} icon={toggleButton.icon} onclick={handleToggleVisibility} />
|
<CircleIconButton title={toggleButton.label} icon={toggleButton.icon} onclick={handleToggleVisibility} />
|
||||||
</div>
|
</div>
|
||||||
{#if !showLoadingSpinner}
|
<Button loading={showLoadingSpinner} onclick={handleSaveVisibility} size="small">{$t('done')}</Button>
|
||||||
<Button onclick={handleSaveVisibility} size="sm" rounded="lg">{$t('done')}</Button>
|
|
||||||
{:else}
|
|
||||||
<LoadingSpinner />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -6,13 +6,13 @@
|
|||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
|
import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
|
||||||
|
import { Button } from '@immich/ui';
|
||||||
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
|
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
@ -108,10 +108,9 @@
|
|||||||
<div></div>
|
<div></div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<Button size="sm" disabled={!hasSelection} onclick={handleMerge}>
|
<Button leadingIcon={mdiMerge} size="small" shape="round" disabled={!hasSelection} onclick={handleMerge}>
|
||||||
<Icon path={mdiMerge} size={18} />
|
{$t('merge')}
|
||||||
<span class="ms-2">{$t('merge')}</span></Button
|
</Button>
|
||||||
>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
<section class="px-[70px] pt-[100px]">
|
<section class="px-[70px] pt-[100px]">
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
|
||||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import {
|
import {
|
||||||
@ -9,14 +8,13 @@
|
|||||||
type AssetFaceUpdateItem,
|
type AssetFaceUpdateItem,
|
||||||
type PersonResponseDto,
|
type PersonResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
import { Button } from '@immich/ui';
|
||||||
import { mdiMerge, mdiPlus } from '@mdi/js';
|
import { mdiMerge, mdiPlus } from '@mdi/js';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
|
||||||
import ControlAppBar from '../shared-components/control-app-bar.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 { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import FaceThumbnail from './face-thumbnail.svelte';
|
import FaceThumbnail from './face-thumbnail.svelte';
|
||||||
import PeopleList from './people-list.svelte';
|
import PeopleList from './people-list.svelte';
|
||||||
@ -130,33 +128,27 @@
|
|||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
|
shape="round"
|
||||||
title={$t('create_new_person_hint')}
|
title={$t('create_new_person_hint')}
|
||||||
size="sm"
|
leadingIcon={mdiPlus}
|
||||||
|
loading={showLoadingSpinnerCreate}
|
||||||
|
size="small"
|
||||||
disabled={disableButtons || hasSelection}
|
disabled={disableButtons || hasSelection}
|
||||||
onclick={handleCreate}
|
onclick={handleCreate}
|
||||||
>
|
>
|
||||||
{#if !showLoadingSpinnerCreate}
|
{$t('create_new_person')}</Button
|
||||||
<Icon path={mdiPlus} size={18} />
|
|
||||||
{:else}
|
|
||||||
<LoadingSpinner />
|
|
||||||
{/if}
|
|
||||||
<span class="ms-2"> {$t('create_new_person')}</span></Button
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="small"
|
||||||
|
shape="round"
|
||||||
title={$t('reassing_hint')}
|
title={$t('reassing_hint')}
|
||||||
|
leadingIcon={mdiMerge}
|
||||||
|
loading={showLoadingSpinnerReassign}
|
||||||
disabled={disableButtons || !hasSelection}
|
disabled={disableButtons || !hasSelection}
|
||||||
onclick={handleReassign}
|
onclick={handleReassign}
|
||||||
>
|
>
|
||||||
{#if !showLoadingSpinnerReassign}
|
{$t('reassign')}
|
||||||
<div>
|
</Button>
|
||||||
<Icon path={mdiMerge} size={18} class="rotate-180" />
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<LoadingSpinner />
|
|
||||||
{/if}
|
|
||||||
<span class="ms-2"> {$t('reassign')}</span></Button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ControlAppBar>
|
</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