Compare commits

..

1 Commits

Author SHA1 Message Date
Jason Rasmussen
9a35fc8631 feat: plugins 2025-10-21 16:45:23 -04:00
58 changed files with 784 additions and 504 deletions

View File

@@ -54,10 +54,16 @@ jobs:
issues: write issues: write
discussions: write discussions: write
steps: steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Close issue - name: Close issue
if: ${{ github.event_name == 'issues' }} if: ${{ github.event_name == 'issues' }}
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ steps.token.outputs.token }}
NODE_ID: ${{ github.event.issue.node_id }} NODE_ID: ${{ github.event.issue.node_id }}
run: | run: |
gh api graphql \ gh api graphql \
@@ -83,7 +89,7 @@ jobs:
- name: Close discussion - name: Close discussion
if: ${{ github.event_name == 'discussion' && github.event.discussion.category.name == 'Feature Request' }} if: ${{ github.event_name == 'discussion' && github.event.discussion.category.name == 'Feature Request' }}
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ steps.token.outputs.token }}
NODE_ID: ${{ github.event.discussion.node_id }} NODE_ID: ${{ github.event.discussion.node_id }}
run: | run: |
gh api graphql \ gh api graphql \

View File

@@ -182,7 +182,7 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
working-directory: 'deployment/modules/cloudflare/docs' working-directory: 'deployment/modules/cloudflare/docs'
run: 'mise run tf output -- -json' run: 'mise run tf output -json'
- name: Output Cleaning - name: Output Cleaning
id: clean id: clean

View File

@@ -36,7 +36,7 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }} TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
working-directory: 'deployment/modules/cloudflare/docs' working-directory: 'deployment/modules/cloudflare/docs'
run: 'mise run tf destroy -- -refresh=false' run: 'mise run tf destroy -refresh=false'
- name: Comment - name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0 uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0

View File

@@ -119,6 +119,5 @@ export const deviceDto = {
isPendingSyncReset: false, isPendingSyncReset: false,
deviceOS: '', deviceOS: '',
deviceType: '', deviceType: '',
appVersion: null,
}, },
}; };

View File

@@ -474,7 +474,6 @@
"app_bar_signout_dialog_title": "Sign out", "app_bar_signout_dialog_title": "Sign out",
"app_download_links": "App Download Links", "app_download_links": "App Download Links",
"app_settings": "App Settings", "app_settings": "App Settings",
"app_stores": "App Stores",
"app_update_available": "App update is available", "app_update_available": "App update is available",
"appears_in": "Appears in", "appears_in": "Appears in",
"apply_count": "Apply ({count, number})", "apply_count": "Apply ({count, number})",
@@ -746,7 +745,6 @@
"create": "Create", "create": "Create",
"create_album": "Create album", "create_album": "Create album",
"create_album_page_untitled": "Untitled", "create_album_page_untitled": "Untitled",
"create_api_key": "Create API key",
"create_library": "Create Library", "create_library": "Create Library",
"create_link": "Create link", "create_link": "Create link",
"create_link_to_share": "Create link to share", "create_link_to_share": "Create link to share",
@@ -1353,7 +1351,7 @@
"minutes": "Minutes", "minutes": "Minutes",
"missing": "Missing", "missing": "Missing",
"mobile_app": "Mobile App", "mobile_app": "Mobile App",
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options", "mobile_app_download_onboarding_note": "You can access these options again from the Utilities page.",
"model": "Model", "model": "Model",
"month": "Month", "month": "Month",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
@@ -1435,7 +1433,7 @@
"notifications_setting_description": "Manage notifications", "notifications_setting_description": "Manage notifications",
"oauth": "OAuth", "oauth": "OAuth",
"obtainium_configurator": "Obtainium Configurator", "obtainium_configurator": "Obtainium Configurator",
"obtainium_configurator_instructions": "Use Obtainium to install and update the Android app directly from Immich GitHub's release. Create an API key and select a variant to create your Obtainium configuration link", "obtainium_configurator_instructions": "Please create an API key and select a variant to create your Obtainium configuration link.",
"official_immich_resources": "Official Immich Resources", "official_immich_resources": "Official Immich Resources",
"offline": "Offline", "offline": "Offline",
"offset": "Offset", "offset": "Offset",

View File

@@ -2,8 +2,8 @@
node = "22.20.0" node = "22.20.0"
flutter = "3.35.6" flutter = "3.35.6"
pnpm = "10.18.1" pnpm = "10.18.1"
terragrunt = "0.91.2" terragrunt = "0.58.12"
opentofu = "1.10.6" opentofu = "1.7.1"
[tools."github:CQLabs/homebrew-dcm"] [tools."github:CQLabs/homebrew-dcm"]
version = "1.30.0" version = "1.30.0"

View File

@@ -282,7 +282,6 @@ Class | Method | HTTP request | Description
*UsersAdminApi* | [**deleteUserAdmin**](doc//UsersAdminApi.md#deleteuseradmin) | **DELETE** /admin/users/{id} | *UsersAdminApi* | [**deleteUserAdmin**](doc//UsersAdminApi.md#deleteuseradmin) | **DELETE** /admin/users/{id} |
*UsersAdminApi* | [**getUserAdmin**](doc//UsersAdminApi.md#getuseradmin) | **GET** /admin/users/{id} | *UsersAdminApi* | [**getUserAdmin**](doc//UsersAdminApi.md#getuseradmin) | **GET** /admin/users/{id} |
*UsersAdminApi* | [**getUserPreferencesAdmin**](doc//UsersAdminApi.md#getuserpreferencesadmin) | **GET** /admin/users/{id}/preferences | *UsersAdminApi* | [**getUserPreferencesAdmin**](doc//UsersAdminApi.md#getuserpreferencesadmin) | **GET** /admin/users/{id}/preferences |
*UsersAdminApi* | [**getUserSessionsAdmin**](doc//UsersAdminApi.md#getusersessionsadmin) | **GET** /admin/users/{id}/sessions |
*UsersAdminApi* | [**getUserStatisticsAdmin**](doc//UsersAdminApi.md#getuserstatisticsadmin) | **GET** /admin/users/{id}/statistics | *UsersAdminApi* | [**getUserStatisticsAdmin**](doc//UsersAdminApi.md#getuserstatisticsadmin) | **GET** /admin/users/{id}/statistics |
*UsersAdminApi* | [**restoreUserAdmin**](doc//UsersAdminApi.md#restoreuseradmin) | **POST** /admin/users/{id}/restore | *UsersAdminApi* | [**restoreUserAdmin**](doc//UsersAdminApi.md#restoreuseradmin) | **POST** /admin/users/{id}/restore |
*UsersAdminApi* | [**searchUsersAdmin**](doc//UsersAdminApi.md#searchusersadmin) | **GET** /admin/users | *UsersAdminApi* | [**searchUsersAdmin**](doc//UsersAdminApi.md#searchusersadmin) | **GET** /admin/users |

View File

@@ -231,62 +231,6 @@ class UsersAdminApi {
return null; return null;
} }
/// This endpoint is an admin-only route, and requires the `adminSession.read` permission.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getUserSessionsAdminWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/users/{id}/sessions'
.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,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This endpoint is an admin-only route, and requires the `adminSession.read` permission.
///
/// Parameters:
///
/// * [String] id (required):
Future<List<SessionResponseDto>?> getUserSessionsAdmin(String id,) async {
final response = await getUserSessionsAdminWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<SessionResponseDto>') as List)
.cast<SessionResponseDto>()
.toList(growable: false);
}
return null;
}
/// This endpoint is an admin-only route, and requires the `adminUser.read` permission. /// This endpoint is an admin-only route, and requires the `adminUser.read` permission.
/// ///
/// Note: This method returns the HTTP [Response]. /// Note: This method returns the HTTP [Response].

View File

@@ -150,7 +150,6 @@ class Permission {
static const adminUserPeriodRead = Permission._(r'adminUser.read'); static const adminUserPeriodRead = Permission._(r'adminUser.read');
static const adminUserPeriodUpdate = Permission._(r'adminUser.update'); static const adminUserPeriodUpdate = Permission._(r'adminUser.update');
static const adminUserPeriodDelete = Permission._(r'adminUser.delete'); static const adminUserPeriodDelete = Permission._(r'adminUser.delete');
static const adminSessionPeriodRead = Permission._(r'adminSession.read');
static const adminAuthPeriodUnlinkAll = Permission._(r'adminAuth.unlinkAll'); static const adminAuthPeriodUnlinkAll = Permission._(r'adminAuth.unlinkAll');
/// List of all possible values in this [enum][Permission]. /// List of all possible values in this [enum][Permission].
@@ -282,7 +281,6 @@ class Permission {
adminUserPeriodRead, adminUserPeriodRead,
adminUserPeriodUpdate, adminUserPeriodUpdate,
adminUserPeriodDelete, adminUserPeriodDelete,
adminSessionPeriodRead,
adminAuthPeriodUnlinkAll, adminAuthPeriodUnlinkAll,
]; ];
@@ -449,7 +447,6 @@ class PermissionTypeTransformer {
case r'adminUser.read': return Permission.adminUserPeriodRead; case r'adminUser.read': return Permission.adminUserPeriodRead;
case r'adminUser.update': return Permission.adminUserPeriodUpdate; case r'adminUser.update': return Permission.adminUserPeriodUpdate;
case r'adminUser.delete': return Permission.adminUserPeriodDelete; case r'adminUser.delete': return Permission.adminUserPeriodDelete;
case r'adminSession.read': return Permission.adminSessionPeriodRead;
case r'adminAuth.unlinkAll': return Permission.adminAuthPeriodUnlinkAll; case r'adminAuth.unlinkAll': return Permission.adminAuthPeriodUnlinkAll;
default: default:
if (!allowNull) { if (!allowNull) {

View File

@@ -13,7 +13,6 @@ part of openapi.api;
class SessionCreateResponseDto { class SessionCreateResponseDto {
/// Returns a new [SessionCreateResponseDto] instance. /// Returns a new [SessionCreateResponseDto] instance.
SessionCreateResponseDto({ SessionCreateResponseDto({
required this.appVersion,
required this.createdAt, required this.createdAt,
required this.current, required this.current,
required this.deviceOS, required this.deviceOS,
@@ -25,8 +24,6 @@ class SessionCreateResponseDto {
required this.updatedAt, required this.updatedAt,
}); });
String? appVersion;
String createdAt; String createdAt;
bool current; bool current;
@@ -53,7 +50,6 @@ class SessionCreateResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is SessionCreateResponseDto && bool operator ==(Object other) => identical(this, other) || other is SessionCreateResponseDto &&
other.appVersion == appVersion &&
other.createdAt == createdAt && other.createdAt == createdAt &&
other.current == current && other.current == current &&
other.deviceOS == deviceOS && other.deviceOS == deviceOS &&
@@ -67,7 +63,6 @@ class SessionCreateResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(appVersion == null ? 0 : appVersion!.hashCode) +
(createdAt.hashCode) + (createdAt.hashCode) +
(current.hashCode) + (current.hashCode) +
(deviceOS.hashCode) + (deviceOS.hashCode) +
@@ -79,15 +74,10 @@ class SessionCreateResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'SessionCreateResponseDto[appVersion=$appVersion, createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, token=$token, updatedAt=$updatedAt]'; String toString() => 'SessionCreateResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, token=$token, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.appVersion != null) {
json[r'appVersion'] = this.appVersion;
} else {
// json[r'appVersion'] = null;
}
json[r'createdAt'] = this.createdAt; json[r'createdAt'] = this.createdAt;
json[r'current'] = this.current; json[r'current'] = this.current;
json[r'deviceOS'] = this.deviceOS; json[r'deviceOS'] = this.deviceOS;
@@ -113,7 +103,6 @@ class SessionCreateResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return SessionCreateResponseDto( return SessionCreateResponseDto(
appVersion: mapValueOfType<String>(json, r'appVersion'),
createdAt: mapValueOfType<String>(json, r'createdAt')!, createdAt: mapValueOfType<String>(json, r'createdAt')!,
current: mapValueOfType<bool>(json, r'current')!, current: mapValueOfType<bool>(json, r'current')!,
deviceOS: mapValueOfType<String>(json, r'deviceOS')!, deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
@@ -170,7 +159,6 @@ class SessionCreateResponseDto {
/// 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>{
'appVersion',
'createdAt', 'createdAt',
'current', 'current',
'deviceOS', 'deviceOS',

View File

@@ -13,7 +13,6 @@ part of openapi.api;
class SessionResponseDto { class SessionResponseDto {
/// Returns a new [SessionResponseDto] instance. /// Returns a new [SessionResponseDto] instance.
SessionResponseDto({ SessionResponseDto({
required this.appVersion,
required this.createdAt, required this.createdAt,
required this.current, required this.current,
required this.deviceOS, required this.deviceOS,
@@ -24,8 +23,6 @@ class SessionResponseDto {
required this.updatedAt, required this.updatedAt,
}); });
String? appVersion;
String createdAt; String createdAt;
bool current; bool current;
@@ -50,7 +47,6 @@ class SessionResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is SessionResponseDto && bool operator ==(Object other) => identical(this, other) || other is SessionResponseDto &&
other.appVersion == appVersion &&
other.createdAt == createdAt && other.createdAt == createdAt &&
other.current == current && other.current == current &&
other.deviceOS == deviceOS && other.deviceOS == deviceOS &&
@@ -63,7 +59,6 @@ class SessionResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(appVersion == null ? 0 : appVersion!.hashCode) +
(createdAt.hashCode) + (createdAt.hashCode) +
(current.hashCode) + (current.hashCode) +
(deviceOS.hashCode) + (deviceOS.hashCode) +
@@ -74,15 +69,10 @@ class SessionResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'SessionResponseDto[appVersion=$appVersion, createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, updatedAt=$updatedAt]'; String toString() => 'SessionResponseDto[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, isPendingSyncReset=$isPendingSyncReset, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.appVersion != null) {
json[r'appVersion'] = this.appVersion;
} else {
// json[r'appVersion'] = null;
}
json[r'createdAt'] = this.createdAt; json[r'createdAt'] = this.createdAt;
json[r'current'] = this.current; json[r'current'] = this.current;
json[r'deviceOS'] = this.deviceOS; json[r'deviceOS'] = this.deviceOS;
@@ -107,7 +97,6 @@ class SessionResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return SessionResponseDto( return SessionResponseDto(
appVersion: mapValueOfType<String>(json, r'appVersion'),
createdAt: mapValueOfType<String>(json, r'createdAt')!, createdAt: mapValueOfType<String>(json, r'createdAt')!,
current: mapValueOfType<bool>(json, r'current')!, current: mapValueOfType<bool>(json, r'current')!,
deviceOS: mapValueOfType<String>(json, r'deviceOS')!, deviceOS: mapValueOfType<String>(json, r'deviceOS')!,
@@ -163,7 +152,6 @@ class SessionResponseDto {
/// 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>{
'appVersion',
'createdAt', 'createdAt',
'current', 'current',
'deviceOS', 'deviceOS',

View File

@@ -773,54 +773,6 @@
"description": "This endpoint is an admin-only route, and requires the `adminUser.delete` permission." "description": "This endpoint is an admin-only route, and requires the `adminUser.delete` permission."
} }
}, },
"/admin/users/{id}/sessions": {
"get": {
"operationId": "getUserSessionsAdmin",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/SessionResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Users (admin)"
],
"x-immich-admin-only": true,
"x-immich-permission": "adminSession.read",
"description": "This endpoint is an admin-only route, and requires the `adminSession.read` permission."
}
},
"/admin/users/{id}/statistics": { "/admin/users/{id}/statistics": {
"get": { "get": {
"operationId": "getUserStatisticsAdmin", "operationId": "getUserStatisticsAdmin",
@@ -13315,7 +13267,6 @@
"adminUser.read", "adminUser.read",
"adminUser.update", "adminUser.update",
"adminUser.delete", "adminUser.delete",
"adminSession.read",
"adminAuth.unlinkAll" "adminAuth.unlinkAll"
], ],
"type": "string" "type": "string"
@@ -14352,10 +14303,6 @@
}, },
"SessionCreateResponseDto": { "SessionCreateResponseDto": {
"properties": { "properties": {
"appVersion": {
"nullable": true,
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
@@ -14385,7 +14332,6 @@
} }
}, },
"required": [ "required": [
"appVersion",
"createdAt", "createdAt",
"current", "current",
"deviceOS", "deviceOS",
@@ -14399,10 +14345,6 @@
}, },
"SessionResponseDto": { "SessionResponseDto": {
"properties": { "properties": {
"appVersion": {
"nullable": true,
"type": "string"
},
"createdAt": { "createdAt": {
"type": "string" "type": "string"
}, },
@@ -14429,7 +14371,6 @@
} }
}, },
"required": [ "required": [
"appVersion",
"createdAt", "createdAt",
"current", "current",
"deviceOS", "deviceOS",

View File

@@ -244,17 +244,6 @@ export type UserPreferencesUpdateDto = {
sharedLinks?: SharedLinksUpdate; sharedLinks?: SharedLinksUpdate;
tags?: TagsUpdate; tags?: TagsUpdate;
}; };
export type SessionResponseDto = {
appVersion: string | null;
createdAt: string;
current: boolean;
deviceOS: string;
deviceType: string;
expiresAt?: string;
id: string;
isPendingSyncReset: boolean;
updatedAt: string;
};
export type AssetStatsResponseDto = { export type AssetStatsResponseDto = {
images: number; images: number;
total: number; total: number;
@@ -1203,6 +1192,16 @@ export type ServerVersionHistoryResponseDto = {
id: string; id: string;
version: string; version: string;
}; };
export type SessionResponseDto = {
createdAt: string;
current: boolean;
deviceOS: string;
deviceType: string;
expiresAt?: string;
id: string;
isPendingSyncReset: boolean;
updatedAt: string;
};
export type SessionCreateDto = { export type SessionCreateDto = {
deviceOS?: string; deviceOS?: string;
deviceType?: string; deviceType?: string;
@@ -1210,7 +1209,6 @@ export type SessionCreateDto = {
duration?: number; duration?: number;
}; };
export type SessionCreateResponseDto = { export type SessionCreateResponseDto = {
appVersion: string | null;
createdAt: string; createdAt: string;
current: boolean; current: boolean;
deviceOS: string; deviceOS: string;
@@ -1855,19 +1853,6 @@ export function restoreUserAdmin({ id }: {
method: "POST" method: "POST"
})); }));
} }
/**
* This endpoint is an admin-only route, and requires the `adminSession.read` permission.
*/
export function getUserSessionsAdmin({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SessionResponseDto[];
}>(`/admin/users/${encodeURIComponent(id)}/sessions`, {
...opts
}));
}
/** /**
* This endpoint is an admin-only route, and requires the `adminUser.read` permission. * This endpoint is an admin-only route, and requires the `adminUser.read` permission.
*/ */
@@ -4845,7 +4830,6 @@ export enum Permission {
AdminUserRead = "adminUser.read", AdminUserRead = "adminUser.read",
AdminUserUpdate = "adminUser.update", AdminUserUpdate = "adminUser.update",
AdminUserDelete = "adminUser.delete", AdminUserDelete = "adminUser.delete",
AdminSessionRead = "adminSession.read",
AdminAuthUnlinkAll = "adminAuth.unlinkAll" AdminAuthUnlinkAll = "adminAuth.unlinkAll"
} }
export enum AssetMetadataKey { export enum AssetMetadataKey {

2
plugins/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist

26
plugins/LICENSE Normal file
View File

@@ -0,0 +1,26 @@
Copyright 2024, The Extism Authors.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

12
plugins/esbuild.js Normal file
View File

@@ -0,0 +1,12 @@
const esbuild = require('esbuild');
esbuild
.build({
entryPoints: ['src/index.ts'],
outdir: 'dist',
bundle: true,
sourcemap: true,
minify: false, // might want to use true for production build
format: 'cjs', // needs to be CJS for now
target: ['es2020'] // don't go over es2020 because quickjs doesn't support it
})

443
plugins/package-lock.json generated Normal file
View File

@@ -0,0 +1,443 @@
{
"name": "js-pdk-template",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "js-pdk-template",
"version": "1.0.0",
"license": "BSD-3-Clause",
"devDependencies": {
"@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6",
"typescript": "^5.3.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@extism/js-pdk": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.0.1.tgz",
"integrity": "sha512-YJWfHGeOuJnQw4V8NPNHvbSr6S8iDd2Ga6VEukwlRP7tu62ozTxIgokYw8i+rajD/16zz/gK0KYARBpm2qPAmQ==",
"dev": true
},
"node_modules/esbuild": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.19.12",
"@esbuild/android-arm": "0.19.12",
"@esbuild/android-arm64": "0.19.12",
"@esbuild/android-x64": "0.19.12",
"@esbuild/darwin-arm64": "0.19.12",
"@esbuild/darwin-x64": "0.19.12",
"@esbuild/freebsd-arm64": "0.19.12",
"@esbuild/freebsd-x64": "0.19.12",
"@esbuild/linux-arm": "0.19.12",
"@esbuild/linux-arm64": "0.19.12",
"@esbuild/linux-ia32": "0.19.12",
"@esbuild/linux-loong64": "0.19.12",
"@esbuild/linux-mips64el": "0.19.12",
"@esbuild/linux-ppc64": "0.19.12",
"@esbuild/linux-riscv64": "0.19.12",
"@esbuild/linux-s390x": "0.19.12",
"@esbuild/linux-x64": "0.19.12",
"@esbuild/netbsd-x64": "0.19.12",
"@esbuild/openbsd-x64": "0.19.12",
"@esbuild/sunos-x64": "0.19.12",
"@esbuild/win32-arm64": "0.19.12",
"@esbuild/win32-ia32": "0.19.12",
"@esbuild/win32-x64": "0.19.12"
}
},
"node_modules/typescript": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

19
plugins/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "plugins",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"build": "pnpm build:tsc && pnpm build:wasm",
"build:tsc": "tsc --noEmit && node esbuild.js",
"build:wasm": "extism-js dist/index.js -i src/index.d.ts -o dist/plugin.wasm"
},
"keywords": [],
"author": "",
"license": "BSD-3-Clause",
"devDependencies": {
"@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6",
"typescript": "^5.3.2"
}
}

9
plugins/src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare module 'main' {
export function archiveAssetAction(): I32;
}
declare module 'extism:host' {
interface user {
updateAsset(ptr: PTR): I32;
}
}

16
plugins/src/index.ts Normal file
View File

@@ -0,0 +1,16 @@
const { updateAsset } = Host.getFunctions();
export function archiveAssetAction() {
const event = JSON.parse(Host.inputString());
const ptr = Memory.fromString(
JSON.stringify({
id: event.asset.id,
visibility: 'archive',
})
);
updateAsset(ptr.offset);
ptr.free();
return 0;
}

24
plugins/tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es2020", // Specify ECMAScript target version
"module": "commonjs", // Specify module code generation
"lib": [
"es2020"
], // Specify a list of library files to be included in the compilation
"types": [
"@extism/js-pdk",
"./src/index.d.ts"
], // Specify a list of type definition files to be included in the compilation
"strict": true, // Enable all strict type-checking options
"esModuleInterop": true, // Enables compatibility with Babel-style module imports
"skipLibCheck": true, // Skip type checking of declaration files
"allowJs": true, // Allow JavaScript files to be compiled
"noEmit": true // Do not emit outputs (no .js or .d.ts files)
},
"include": [
"src/**/*.ts" // Include all TypeScript files in src directory
],
"exclude": [
"node_modules" // Exclude the node_modules directory
]
}

51
pnpm-lock.yaml generated
View File

@@ -299,8 +299,23 @@ importers:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.9.3 version: 5.9.3
plugins:
devDependencies:
'@extism/js-pdk':
specifier: ^1.0.1
version: 1.1.1
esbuild:
specifier: ^0.19.6
version: 0.19.12
typescript:
specifier: ^5.3.2
version: 5.9.3
server: server:
dependencies: dependencies:
'@extism/extism':
specifier: 2.0.0-rc13
version: 2.0.0-rc13
'@nestjs/bullmq': '@nestjs/bullmq':
specifier: ^11.0.1 specifier: ^11.0.1
version: 11.0.4(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bullmq@5.61.0) version: 11.0.4(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bullmq@5.61.0)
@@ -789,9 +804,6 @@ importers:
'@koddsson/eslint-plugin-tscompat': '@koddsson/eslint-plugin-tscompat':
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) version: 0.2.0(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3)
'@rollup/plugin-replace':
specifier: ^6.0.2
version: 6.0.2(rollup@4.52.5)
'@socket.io/component-emitter': '@socket.io/component-emitter':
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.2 version: 3.1.2
@@ -2528,6 +2540,12 @@ packages:
resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@extism/extism@2.0.0-rc13':
resolution: {integrity: sha512-iQ3mrPKOC0WMZ94fuJrKbJmMyz4LQ9Abf8gd4F5ShxKWa+cRKcVzk0EqRQsp5xXsQ2dO3zJTiA6eTc4Ihf7k+A==}
'@extism/js-pdk@1.1.1':
resolution: {integrity: sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==}
'@faker-js/faker@10.1.0': '@faker-js/faker@10.1.0':
resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==} resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==}
engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'}
@@ -3684,15 +3702,6 @@ packages:
peerDependencies: peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc react: ^18.0 || ^19.0 || ^19.0.0-rc
'@rollup/plugin-replace@6.0.2':
resolution: {integrity: sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/pluginutils@5.3.0': '@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -10960,6 +10969,9 @@ packages:
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
urlpattern-polyfill@8.0.2:
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
utf8-byte-length@1.0.5: utf8-byte-length@1.0.5:
resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==}
@@ -14020,6 +14032,12 @@ snapshots:
'@eslint/core': 0.16.0 '@eslint/core': 0.16.0
levn: 0.4.1 levn: 0.4.1
'@extism/extism@2.0.0-rc13': {}
'@extism/js-pdk@1.1.1':
dependencies:
urlpattern-polyfill: 8.0.2
'@faker-js/faker@10.1.0': {} '@faker-js/faker@10.1.0': {}
'@fig/complete-commander@3.2.0(commander@11.1.0)': '@fig/complete-commander@3.2.0(commander@11.1.0)':
@@ -15302,13 +15320,6 @@ snapshots:
dependencies: dependencies:
react: 19.2.0 react: 19.2.0
'@rollup/plugin-replace@6.0.2(rollup@4.52.5)':
dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.52.5)
magic-string: 0.30.19
optionalDependencies:
rollup: 4.52.5
'@rollup/pluginutils@5.3.0(rollup@4.52.5)': '@rollup/pluginutils@5.3.0(rollup@4.52.5)':
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@@ -23944,6 +23955,8 @@ snapshots:
punycode: 1.4.1 punycode: 1.4.1
qs: 6.14.0 qs: 6.14.0
urlpattern-polyfill@8.0.2: {}
utf8-byte-length@1.0.5: {} utf8-byte-length@1.0.5: {}
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}

View File

@@ -4,6 +4,7 @@ packages:
- e2e - e2e
- open-api/typescript-sdk - open-api/typescript-sdk
- server - server
- plugins
- web - web
- .github - .github
ignoredBuiltDependencies: ignoredBuiltDependencies:

View File

@@ -34,6 +34,7 @@
"email:dev": "email dev -p 3050 --dir src/emails" "email:dev": "email dev -p 3050 --dir src/emails"
}, },
"dependencies": { "dependencies": {
"@extism/extism": "2.0.0-rc13",
"@nestjs/bullmq": "^11.0.1", "@nestjs/bullmq": "^11.0.1",
"@nestjs/common": "^11.0.4", "@nestjs/common": "^11.0.4",
"@nestjs/core": "^11.0.4", "@nestjs/core": "^11.0.4",

View File

@@ -2,7 +2,6 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put,
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto } from 'src/dtos/session.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
import { import {
UserAdminCreateDto, UserAdminCreateDto,
@@ -59,12 +58,6 @@ export class UserAdminController {
return this.service.delete(auth, id, dto); return this.service.delete(auth, id, dto);
} }
@Get(':id/sessions')
@Authenticated({ permission: Permission.AdminSessionRead, admin: true })
getUserSessionsAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SessionResponseDto[]> {
return this.service.getSessions(auth, id);
}
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated({ permission: Permission.AdminUserRead, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
getUserStatisticsAdmin( getUserStatisticsAdmin(

View File

@@ -238,7 +238,6 @@ export type Session = {
expiresAt: Date | null; expiresAt: Date | null;
deviceOS: string; deviceOS: string;
deviceType: string; deviceType: string;
appVersion: string | null;
pinExpiresAt: Date | null; pinExpiresAt: Date | null;
isPendingSyncReset: boolean; isPendingSyncReset: boolean;
}; };
@@ -309,7 +308,7 @@ export const columns = {
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'], assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
authApiKey: ['api_key.id', 'api_key.permissions'], authApiKey: ['api_key.id', 'api_key.permissions'],
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.appVersion'], authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt'],
authSharedLink: [ authSharedLink: [
'shared_link.id', 'shared_link.id',
'shared_link.userId', 'shared_link.userId',

View File

@@ -195,10 +195,4 @@ export class EnvDto {
@IsString() @IsString()
@Optional() @Optional()
REDIS_URL?: string; REDIS_URL?: string;
@ValidateBoolean({ optional: true })
IMMICH_DEV_CORS_ALL_ORIGINS?: boolean;
@ValidateBoolean({ optional: true })
IMMICH_DEV_CORS_CREDENTIALS?: boolean;
} }

View File

@@ -34,7 +34,6 @@ export class SessionResponseDto {
current!: boolean; current!: boolean;
deviceType!: string; deviceType!: string;
deviceOS!: string; deviceOS!: string;
appVersion!: string | null;
isPendingSyncReset!: boolean; isPendingSyncReset!: boolean;
} }
@@ -48,7 +47,6 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse
updatedAt: entity.updatedAt.toISOString(), updatedAt: entity.updatedAt.toISOString(),
expiresAt: entity.expiresAt?.toISOString(), expiresAt: entity.expiresAt?.toISOString(),
current: currentId === entity.id, current: currentId === entity.id,
appVersion: entity.appVersion,
deviceOS: entity.deviceOS, deviceOS: entity.deviceOS,
deviceType: entity.deviceType, deviceType: entity.deviceType,
isPendingSyncReset: entity.isPendingSyncReset, isPendingSyncReset: entity.isPendingSyncReset,

View File

@@ -173,7 +173,6 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
const license = metadata.find( const license = metadata.find(
(item): item is UserMetadataItem<UserMetadataKey.License> => item.key === UserMetadataKey.License, (item): item is UserMetadataItem<UserMetadataKey.License> => item.key === UserMetadataKey.License,
)?.value; )?.value;
return { return {
...mapUser(entity), ...mapUser(entity),
storageLabel: entity.storageLabel, storageLabel: entity.storageLabel,

View File

@@ -236,8 +236,6 @@ export enum Permission {
AdminUserUpdate = 'adminUser.update', AdminUserUpdate = 'adminUser.update',
AdminUserDelete = 'adminUser.delete', AdminUserDelete = 'adminUser.delete',
AdminSessionRead = 'adminSession.read',
AdminAuthUnlinkAll = 'adminAuth.unlinkAll', AdminAuthUnlinkAll = 'adminAuth.unlinkAll',
} }

View File

@@ -13,7 +13,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { ApiCustomExtension, ImmichQuery, MetadataKey, Permission } from 'src/enum'; import { ApiCustomExtension, ImmichQuery, MetadataKey, Permission } from 'src/enum';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { AuthService, LoginDetails } from 'src/services/auth.service'; import { AuthService, LoginDetails } from 'src/services/auth.service';
import { getUserAgentDetails } from 'src/utils/request'; import { UAParser } from 'ua-parser-js';
type AdminRoute = { admin?: true }; type AdminRoute = { admin?: true };
type SharedLinkRoute = { sharedLink?: true }; type SharedLinkRoute = { sharedLink?: true };
@@ -56,14 +56,13 @@ export const FileResponse = () =>
export const GetLoginDetails = createParamDecorator((data, context: ExecutionContext): LoginDetails => { export const GetLoginDetails = createParamDecorator((data, context: ExecutionContext): LoginDetails => {
const request = context.switchToHttp().getRequest<Request>(); const request = context.switchToHttp().getRequest<Request>();
const { deviceType, deviceOS, appVersion } = getUserAgentDetails(request.headers); const userAgent = UAParser(request.headers['user-agent']);
return { return {
clientIp: request.ip ?? '', clientIp: request.ip ?? '',
isSecure: request.secure, isSecure: request.secure,
deviceType, deviceType: userAgent.browser.name || userAgent.device.type || (request.headers.devicemodel as string) || '',
deviceOS, deviceOS: userAgent.os.name || (request.headers.devicetype as string) || '',
appVersion,
}; };
}); });
@@ -87,6 +86,7 @@ export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const targets = [context.getHandler()]; const targets = [context.getHandler()];
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AuthRoute, targets); const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AuthRoute, targets);
if (!options) { if (!options) {
return true; return true;

View File

@@ -23,7 +23,6 @@ select
"session"."id", "session"."id",
"session"."updatedAt", "session"."updatedAt",
"session"."pinExpiresAt", "session"."pinExpiresAt",
"session"."appVersion",
( (
select select
to_json(obj) to_json(obj)

View File

@@ -1,6 +1,5 @@
import { RegisterQueueOptions } from '@nestjs/bullmq'; import { RegisterQueueOptions } from '@nestjs/bullmq';
import { Inject, Injectable, Optional } from '@nestjs/common'; import { Inject, Injectable, Optional } from '@nestjs/common';
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
import { QueueOptions } from 'bullmq'; import { QueueOptions } from 'bullmq';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { validateSync } from 'class-validator'; import { validateSync } from 'class-validator';
@@ -31,13 +30,6 @@ export interface EnvData {
configFile?: string; configFile?: string;
logLevel?: LogLevel; logLevel?: LogLevel;
dev: {
cors: {
allOrigins?: boolean;
credentials?: boolean;
};
};
buildMetadata: { buildMetadata: {
build?: string; build?: string;
buildUrl?: string; buildUrl?: string;
@@ -230,13 +222,6 @@ const getEnv = (): EnvData => {
configFile: dto.IMMICH_CONFIG_FILE, configFile: dto.IMMICH_CONFIG_FILE,
logLevel: dto.IMMICH_LOG_LEVEL, logLevel: dto.IMMICH_LOG_LEVEL,
dev: {
cors: {
allOrigins: dto.IMMICH_DEV_CORS_ALL_ORIGINS,
credentials: dto.IMMICH_DEV_CORS_CREDENTIALS,
},
},
buildMetadata: { buildMetadata: {
build: dto.IMMICH_BUILD, build: dto.IMMICH_BUILD,
buildUrl: dto.IMMICH_BUILD_URL, buildUrl: dto.IMMICH_BUILD_URL,
@@ -357,24 +342,6 @@ export class ConfigRepository {
return this.getEnv().environment === ImmichEnvironment.Development; return this.getEnv().environment === ImmichEnvironment.Development;
} }
getCorsOptions(): CorsOptions | undefined {
const options: Partial<CorsOptions> = {};
const env = this.getEnv();
if (env.dev.cors.allOrigins) {
options.origin = (requestOrigin, callback) => {
callback(null, requestOrigin);
};
}
if (env.dev.cors.credentials) {
options.credentials = env.dev.cors.credentials;
}
if (Object.keys(options).length > 0) {
return options;
}
return undefined;
}
getWorker() { getWorker() {
return this.worker; return this.worker;
} }

View File

@@ -33,6 +33,11 @@ type Item<T extends EmitEvent> = {
label: string; label: string;
}; };
type AssetCreateV1 = {
id: string;
ownerId: string;
};
type EventMap = { type EventMap = {
// app events // app events
AppBootstrap: []; AppBootstrap: [];
@@ -53,6 +58,7 @@ type EventMap = {
AlbumInvite: [{ id: string; userId: string }]; AlbumInvite: [{ id: string; userId: string }];
// asset events // asset events
AssetCreate: [{ asset: AssetCreateV1 }];
AssetTag: [{ assetId: string }]; AssetTag: [{ assetId: string }];
AssetUntag: [{ assetId: string }]; AssetUntag: [{ assetId: string }];
AssetHide: [{ assetId: string; userId: string }]; AssetHide: [{ assetId: string; userId: string }];

View File

@@ -1,9 +0,0 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "session" ADD "appVersion" character varying;`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "session" DROP COLUMN "appVersion";`.execute(db);
}

View File

@@ -42,9 +42,6 @@ export class SessionTable {
@Column({ default: '' }) @Column({ default: '' })
deviceOS!: Generated<string>; deviceOS!: Generated<string>;
@Column({ nullable: true })
appVersion!: string | null;
@UpdateIdColumn({ index: true }) @UpdateIdColumn({ index: true })
updateId!: Generated<string>; updateId!: Generated<string>;

View File

@@ -426,6 +426,9 @@ export class AssetMediaService extends BaseService {
} }
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size }); await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size });
await this.eventRepository.emit('AssetCreate', { asset });
await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } }); await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } });
return asset; return asset;

View File

@@ -41,7 +41,6 @@ const loginDetails = {
clientIp: '127.0.0.1', clientIp: '127.0.0.1',
deviceOS: '', deviceOS: '',
deviceType: '', deviceType: '',
appVersion: null,
}; };
const fixtures = { const fixtures = {
@@ -244,7 +243,6 @@ describe(AuthService.name, () => {
updatedAt: session.updatedAt, updatedAt: session.updatedAt,
user: factory.authUser(), user: factory.authUser(),
pinExpiresAt: null, pinExpiresAt: null,
appVersion: null,
}; };
mocks.session.getByToken.mockResolvedValue(sessionWithToken); mocks.session.getByToken.mockResolvedValue(sessionWithToken);
@@ -410,7 +408,6 @@ describe(AuthService.name, () => {
updatedAt: session.updatedAt, updatedAt: session.updatedAt,
user: factory.authUser(), user: factory.authUser(),
pinExpiresAt: null, pinExpiresAt: null,
appVersion: null,
}; };
mocks.session.getByToken.mockResolvedValue(sessionWithToken); mocks.session.getByToken.mockResolvedValue(sessionWithToken);
@@ -438,7 +435,6 @@ describe(AuthService.name, () => {
user: factory.authUser(), user: factory.authUser(),
isPendingSyncReset: false, isPendingSyncReset: false,
pinExpiresAt: null, pinExpiresAt: null,
appVersion: null,
}; };
mocks.session.getByToken.mockResolvedValue(sessionWithToken); mocks.session.getByToken.mockResolvedValue(sessionWithToken);
@@ -460,7 +456,6 @@ describe(AuthService.name, () => {
user: factory.authUser(), user: factory.authUser(),
isPendingSyncReset: false, isPendingSyncReset: false,
pinExpiresAt: null, pinExpiresAt: null,
appVersion: null,
}; };
mocks.session.getByToken.mockResolvedValue(sessionWithToken); mocks.session.getByToken.mockResolvedValue(sessionWithToken);

View File

@@ -29,13 +29,11 @@ import { BaseService } from 'src/services/base.service';
import { isGranted } from 'src/utils/access'; import { isGranted } from 'src/utils/access';
import { HumanReadableSize } from 'src/utils/bytes'; import { HumanReadableSize } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { getUserAgentDetails } from 'src/utils/request';
export interface LoginDetails { export interface LoginDetails {
isSecure: boolean; isSecure: boolean;
clientIp: string; clientIp: string;
deviceType: string; deviceType: string;
deviceOS: string; deviceOS: string;
appVersion: string | null;
} }
interface ClaimOptions<T> { interface ClaimOptions<T> {
@@ -220,7 +218,7 @@ export class AuthService extends BaseService {
} }
if (session) { if (session) {
return this.validateSession(session, headers); return this.validateSession(session);
} }
if (apiKey) { if (apiKey) {
@@ -465,22 +463,15 @@ export class AuthService extends BaseService {
return this.cryptoRepository.compareBcrypt(inputSecret, existingHash); return this.cryptoRepository.compareBcrypt(inputSecret, existingHash);
} }
private async validateSession(tokenValue: string, headers: IncomingHttpHeaders): Promise<AuthDto> { private async validateSession(tokenValue: string): Promise<AuthDto> {
const hashedToken = this.cryptoRepository.hashSha256(tokenValue); const hashedToken = this.cryptoRepository.hashSha256(tokenValue);
const session = await this.sessionRepository.getByToken(hashedToken); const session = await this.sessionRepository.getByToken(hashedToken);
if (session?.user) { if (session?.user) {
const { appVersion, deviceOS, deviceType } = getUserAgentDetails(headers);
const now = DateTime.now(); const now = DateTime.now();
const updatedAt = DateTime.fromJSDate(session.updatedAt); const updatedAt = DateTime.fromJSDate(session.updatedAt);
const diff = now.diff(updatedAt, ['hours']); const diff = now.diff(updatedAt, ['hours']);
if (diff.hours > 1 || appVersion != session.appVersion) { if (diff.hours > 1) {
await this.sessionRepository.update(session.id, { await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() });
id: session.id,
updatedAt: new Date(),
appVersion,
deviceOS,
deviceType,
});
} }
// Pin check // Pin check
@@ -538,7 +529,6 @@ export class AuthService extends BaseService {
token: tokenHashed, token: tokenHashed,
deviceOS: loginDetails.deviceOS, deviceOS: loginDetails.deviceOS,
deviceType: loginDetails.deviceType, deviceType: loginDetails.deviceType,
appVersion: loginDetails.appVersion,
userId: user.id, userId: user.id,
}); });

View File

@@ -22,6 +22,7 @@ import { NotificationAdminService } from 'src/services/notification-admin.servic
import { NotificationService } from 'src/services/notification.service'; import { NotificationService } from 'src/services/notification.service';
import { PartnerService } from 'src/services/partner.service'; import { PartnerService } from 'src/services/partner.service';
import { PersonService } from 'src/services/person.service'; import { PersonService } from 'src/services/person.service';
import { PluginService } from 'src/services/plugin.service';
import { SearchService } from 'src/services/search.service'; import { SearchService } from 'src/services/search.service';
import { ServerService } from 'src/services/server.service'; import { ServerService } from 'src/services/server.service';
import { SessionService } from 'src/services/session.service'; import { SessionService } from 'src/services/session.service';
@@ -66,6 +67,7 @@ export const services = [
NotificationAdminService, NotificationAdminService,
PartnerService, PartnerService,
PersonService, PersonService,
PluginService,
SearchService, SearchService,
ServerService, ServerService,
SessionService, SessionService,

View File

@@ -0,0 +1,31 @@
import { CurrentPlugin, newPlugin } from '@extism/extism';
import { Updateable } from 'kysely';
import { resolve } from 'node:path';
import { OnEvent } from 'src/decorators';
import { ArgOf } from 'src/repositories/event.repository';
import { AssetTable } from 'src/schema/tables/asset.table';
import { BaseService } from 'src/services/base.service';
export class PluginService extends BaseService {
@OnEvent({ name: 'AssetCreate' })
async handleAssetCreate({ asset }: ArgOf<'AssetCreate'>) {
console.log(`PluginService.handleAssetCreate: ${asset.id}`);
const corePath = resolve('../plugins/dist/plugin.wasm');
const plugin = await newPlugin(corePath, {
useWasi: true,
functions: {
'extism:host/user': {
updateAsset: (cp: CurrentPlugin, offs: bigint) => this.updateAsset(JSON.parse(cp.read(offs)!.text())),
},
},
});
const event = { asset };
await plugin.call('archiveAssetAction', JSON.stringify(event));
}
async updateAsset(asset: Updateable<AssetTable> & { id: string }) {
console.log(`Updating asset ${asset.id} -- ${JSON.stringify({ ...asset, id: undefined })}`);
await this.assetRepository.update(asset);
}
}

View File

@@ -2,7 +2,6 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/com
import { SALT_ROUNDS } from 'src/constants'; import { SALT_ROUNDS } from 'src/constants';
import { AssetStatsDto, AssetStatsResponseDto, mapStats } from 'src/dtos/asset.dto'; import { AssetStatsDto, AssetStatsResponseDto, mapStats } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
import { import {
UserAdminCreateDto, UserAdminCreateDto,
@@ -120,11 +119,6 @@ export class UserAdminService extends BaseService {
return mapUserAdmin(user); return mapUserAdmin(user);
} }
async getSessions(auth: AuthDto, id: string): Promise<SessionResponseDto[]> {
const sessions = await this.sessionRepository.getByUserId(id);
return sessions.map((session) => mapSession(session));
}
async getStatistics(auth: AuthDto, id: string, dto: AssetStatsDto): Promise<AssetStatsResponseDto> { async getStatistics(auth: AuthDto, id: string, dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
const stats = await this.assetRepository.getStatistics(id, dto); const stats = await this.assetRepository.getStatistics(id, dto);
return mapStats(stats); return mapStats(stats);

View File

@@ -1,22 +1,5 @@
import { IncomingHttpHeaders } from 'node:http';
import { UAParser } from 'ua-parser-js';
export const fromChecksum = (checksum: string): Buffer => { export const fromChecksum = (checksum: string): Buffer => {
return Buffer.from(checksum, checksum.length === 28 ? 'base64' : 'hex'); return Buffer.from(checksum, checksum.length === 28 ? 'base64' : 'hex');
}; };
export const fromMaybeArray = <T>(param: T | T[]) => (Array.isArray(param) ? param[0] : param); export const fromMaybeArray = <T>(param: T | T[]) => (Array.isArray(param) ? param[0] : param);
const getAppVersionFromUA = (ua: string) =>
ua.match(/^Immich_(?:Android|iOS)_(?<appVersion>.+)$/)?.groups?.appVersion ?? null;
export const getUserAgentDetails = (headers: IncomingHttpHeaders) => {
const userAgent = UAParser(headers['user-agent']);
const appVersion = getAppVersionFromUA(headers['user-agent'] ?? '');
return {
deviceType: userAgent.browser.name || userAgent.device.type || (headers['devicemodel'] as string) || '',
deviceOS: userAgent.os.name || (headers['devicetype'] as string) || '',
appVersion,
};
};

View File

@@ -34,17 +34,7 @@ async function bootstrap() {
app.use(cookieParser()); app.use(cookieParser());
app.use(json({ limit: '10mb' })); app.use(json({ limit: '10mb' }));
if (configRepository.isDev()) { if (configRepository.isDev()) {
const options = configRepository.getCorsOptions(); app.enableCors();
if (options) {
logger.warn(`Enabling CORS: ${JSON.stringify(configRepository.getEnv().dev.cors)}`);
logger.warn(
'NOTE: to properly support a fully statically hosted frontend you MUST configure the frontend/backend to be on the same site. i.e. frontend=https://localhost:1234 and backend=http://localhost:2283 or configure TLS',
);
app.enableCors(options);
} else {
logger.warn('Enabling CORS');
app.enableCors();
}
} }
app.useWebSocketAdapter(new WebSocketAdapter(app)); app.useWebSocketAdapter(new WebSocketAdapter(app));
useSwagger(app, { write: configRepository.isDev() }); useSwagger(app, { write: configRepository.isDev() });

View File

@@ -628,7 +628,7 @@ const syncStream = () => {
}; };
const loginDetails = () => { const loginDetails = () => {
return { isSecure: false, clientIp: '', deviceType: '', deviceOS: '', appVersion: null }; return { isSecure: false, clientIp: '', deviceType: '', deviceOS: '' };
}; };
const loginResponse = (): LoginResponseDto => { const loginResponse = (): LoginResponseDto => {

View File

@@ -135,7 +135,6 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
userId: newUuid(), userId: newUuid(),
pinExpiresAt: newDate(), pinExpiresAt: newDate(),
isPendingSyncReset: false, isPendingSyncReset: false,
appVersion: session.appVersion ?? null,
...session, ...session,
}); });

View File

@@ -65,7 +65,6 @@
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@faker-js/faker": "^10.0.0", "@faker-js/faker": "^10.0.0",
"@koddsson/eslint-plugin-tscompat": "^0.2.0", "@koddsson/eslint-plugin-tscompat": "^0.2.0",
"@rollup/plugin-replace": "^6.0.2",
"@socket.io/component-emitter": "^3.1.0", "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/enhanced-img": "^0.8.0", "@sveltejs/enhanced-img": "^0.8.0",

View File

@@ -20,7 +20,7 @@
<AppShellHeader> <AppShellHeader>
<NavigationBar showUploadButton={false} noBorder /> <NavigationBar showUploadButton={false} noBorder />
</AppShellHeader> </AppShellHeader>
<AppShellSidebar bind:open={sidebarStore.isOpen} class="border-none shadow-none"> <AppShellSidebar bind:open={sidebarStore.isOpen}>
<AdminSidebar /> <AdminSidebar />
</AppShellSidebar> </AppShellSidebar>

View File

@@ -6,26 +6,22 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
</script> </script>
<p>{$t('mobile_app_download_onboarding_note')}</p> <HStack wrap>
<HStack>
<Button <Button
size="medium" size="large"
shape="semi-round" shape="semi-round"
fullWidth
onclick={() => modalManager.show(AppDownloadModal, {})}
leadingIcon={mdiCellphoneArrowDownVariant}
>
{$t('app_stores')}
</Button>
<Button
size="medium"
shape="semi-round"
fullWidth
onclick={() => modalManager.show(ObtainiumConfigModal, {})} onclick={() => modalManager.show(ObtainiumConfigModal, {})}
leadingIcon={mdiLinkEdit} leadingIcon={mdiLinkEdit}
> >
{$t('obtainium_configurator')} {$t('obtainium_configurator')}
</Button> </Button>
<Button
size="large"
shape="semi-round"
onclick={() => modalManager.show(AppDownloadModal, {})}
leadingIcon={mdiCellphoneArrowDownVariant}
>
{$t('app_download_links')}
</Button>
</HStack> </HStack>
<p>{$t('mobile_app_download_onboarding_note')}</p>

View File

@@ -18,11 +18,11 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
session: SessionResponseDto; device: SessionResponseDto;
onDelete?: (() => void) | undefined; onDelete?: (() => void) | undefined;
} }
const { session, onDelete = undefined }: Props = $props(); let { device, onDelete = undefined }: Props = $props();
const options: ToRelativeCalendarOptions = { const options: ToRelativeCalendarOptions = {
unit: 'days', unit: 'days',
@@ -32,21 +32,21 @@
<div class="flex w-full flex-row"> <div class="flex w-full flex-row">
<div class="hidden items-center justify-center pe-2 text-primary sm:flex"> <div class="hidden items-center justify-center pe-2 text-primary sm:flex">
{#if session.deviceOS === 'Android'} {#if device.deviceOS === 'Android'}
<Icon icon={mdiAndroid} size="40" /> <Icon icon={mdiAndroid} size="40" />
{:else if session.deviceOS === 'iOS' || session.deviceOS === 'macOS'} {:else if device.deviceOS === 'iOS' || device.deviceOS === 'macOS'}
<Icon icon={mdiApple} size="40" /> <Icon icon={mdiApple} size="40" />
{:else if session.deviceOS.includes('Safari')} {:else if device.deviceOS.includes('Safari')}
<Icon icon={mdiAppleSafari} size="40" /> <Icon icon={mdiAppleSafari} size="40" />
{:else if session.deviceOS.includes('Windows')} {:else if device.deviceOS.includes('Windows')}
<Icon icon={mdiMicrosoftWindows} size="40" /> <Icon icon={mdiMicrosoftWindows} size="40" />
{:else if session.deviceOS === 'Linux'} {:else if device.deviceOS === 'Linux'}
<Icon icon={mdiLinux} size="40" /> <Icon icon={mdiLinux} size="40" />
{:else if session.deviceOS === 'Ubuntu'} {:else if device.deviceOS === 'Ubuntu'}
<Icon icon={mdiUbuntu} size="40" /> <Icon icon={mdiUbuntu} size="40" />
{:else if session.deviceOS === 'Chrome OS' || session.deviceType === 'Chrome' || session.deviceType === 'Chromium' || session.deviceType === 'Mobile Chrome'} {:else if device.deviceOS === 'Chrome OS' || device.deviceType === 'Chrome' || device.deviceType === 'Chromium' || device.deviceType === 'Mobile Chrome'}
<Icon icon={mdiGoogleChrome} size="40" /> <Icon icon={mdiGoogleChrome} size="40" />
{:else if session.deviceOS === 'Google Cast'} {:else if device.deviceOS === 'Google Cast'}
<Icon icon={mdiCast} size="40" /> <Icon icon={mdiCast} size="40" />
{:else} {:else}
<Icon icon={mdiHelp} size="40" /> <Icon icon={mdiHelp} size="40" />
@@ -55,28 +55,24 @@
<div class="flex grow flex-row justify-between gap-1 ps-4 sm:ps-0"> <div class="flex grow flex-row justify-between gap-1 ps-4 sm:ps-0">
<div class="flex flex-col justify-center gap-1 dark:text-white"> <div class="flex flex-col justify-center gap-1 dark:text-white">
<span class="text-sm"> <span class="text-sm">
{#if session.deviceType || session.deviceOS} {#if device.deviceType || device.deviceOS}
<span <span>{device.deviceOS || $t('unknown')}{device.deviceType || $t('unknown')}</span>
>{session.deviceOS || $t('unknown')}{session.deviceType || $t('unknown')}{session.appVersion
? `(v${session.appVersion})`
: ''}</span
>
{:else} {:else}
<span>{$t('unknown')}</span> <span>{$t('unknown')}</span>
{/if} {/if}
</span> </span>
<div class="text-sm"> <div class="text-sm">
<span class="">{$t('last_seen')}</span> <span class="">{$t('last_seen')}</span>
<span>{DateTime.fromISO(session.updatedAt, { locale: $locale }).toRelativeCalendar(options)}</span> <span>{DateTime.fromISO(device.updatedAt, { locale: $locale }).toRelativeCalendar(options)}</span>
<span class="text-xs text-gray-500 dark:text-gray-400"> - </span> <span class="text-xs text-gray-500 dark:text-gray-400"> - </span>
<span class="text-xs text-gray-500 dark:text-gray-400"> <span class="text-xs text-gray-500 dark:text-gray-400">
{DateTime.fromISO(session.updatedAt, { locale: $locale }).toLocaleString(DateTime.DATETIME_MED, { {DateTime.fromISO(device.updatedAt, { locale: $locale }).toLocaleString(DateTime.DATETIME_MED, {
locale: $locale, locale: $locale,
})} })}
</span> </span>
</div> </div>
</div> </div>
{#if !session.current && onDelete} {#if !device.current && onDelete}
<div> <div>
<IconButton <IconButton
color="danger" color="danger"

View File

@@ -14,8 +14,8 @@
const refresh = () => getSessions().then((_devices) => (devices = _devices)); const refresh = () => getSessions().then((_devices) => (devices = _devices));
let currentSession = $derived(devices.find((device) => device.current)); let currentDevice = $derived(devices.find((device) => device.current));
let otherSessions = $derived(devices.filter((device) => !device.current)); let otherDevices = $derived(devices.filter((device) => !device.current));
const handleDelete = async (device: SessionResponseDto) => { const handleDelete = async (device: SessionResponseDto) => {
const isConfirmed = await modalManager.showDialog({ prompt: $t('logout_this_device_confirmation') }); const isConfirmed = await modalManager.showDialog({ prompt: $t('logout_this_device_confirmation') });
@@ -54,22 +54,22 @@
</script> </script>
<section class="my-4"> <section class="my-4">
{#if currentSession} {#if currentDevice}
<div class="mb-6"> <div class="mb-6">
<h3 class="uppercase mb-2 text-xs font-medium text-primary"> <h3 class="uppercase mb-2 text-xs font-medium text-primary">
{$t('current_device')} {$t('current_device')}
</h3> </h3>
<DeviceCard session={currentSession} /> <DeviceCard device={currentDevice} />
</div> </div>
{/if} {/if}
{#if otherSessions.length > 0} {#if otherDevices.length > 0}
<div class="mb-6"> <div class="mb-6">
<h3 class="uppercase mb-2 text-xs font-medium text-primary"> <h3 class="uppercase mb-2 text-xs font-medium text-primary">
{$t('other_devices')} {$t('other_devices')}
</h3> </h3>
{#each otherSessions as session, index (session.id)} {#each otherDevices as device, index (device.id)}
<DeviceCard {session} onDelete={() => handleDelete(session)} /> <DeviceCard {device} onDelete={() => handleDelete(device)} />
{#if index !== otherSessions.length - 1} {#if index !== otherDevices.length - 1}
<hr class="my-3" /> <hr class="my-3" />
{/if} {/if}
{/each} {/each}

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { appStoreBadge, fdroidBadge, Modal, ModalBody, playStoreBadge, Text } from '@immich/ui'; import { appStoreBadge, fdroidBadge, Modal, ModalBody, playStoreBadge } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
onClose: () => void; onClose: () => void;
@@ -9,29 +9,35 @@
<Modal title={$t('app_download_links')} size="large" {onClose}> <Modal title={$t('app_download_links')} size="large" {onClose}>
<ModalBody> <ModalBody>
<div class="sm:grid sm:grid-cols-2 gap-5"> <div class="flex flex-col sm:grid sm:grid-cols-2 gap-5 text-immich-primary dark:text-immich-dark-primary">
<div class="flex flex-col place-items-start"> <div>
<Text>Google Play</Text> <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="fdroid-link">
F-Droid
</label>
<a href="https://f-droid.org/packages/app.alextran.immich/" target="_blank" id="fdroid-link">
<img class="pt-2 pr-10" alt="Get it on F-Droid" src={fdroidBadge} />
</a>
</div>
<div>
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="play-store-link">
Google Play
</label>
<a <a
href="https://play.google.com/store/apps/details?id=app.alextran.immich" href="https://play.google.com/store/apps/details?id=app.alextran.immich"
target="_blank" target="_blank"
id="play-store-link" id="play-store-link"
> >
<img class="w-[200px] mt-2" alt="Get it on Google Play" src={playStoreBadge} /> <img alt="Get it on Google Play" src={playStoreBadge} />
</a> </a>
</div> </div>
<div class="flex flex-col place-items-start"> <div>
<Text>App Store</Text> <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="app-store-link">
App Store
</label>
<a href="https://apps.apple.com/us/app/immich/id1613945652" target="_blank" id="app-store-link"> <a href="https://apps.apple.com/us/app/immich/id1613945652" target="_blank" id="app-store-link">
<img class="w-[200px] mt-2" alt="Download on the App Store" src={appStoreBadge} /> <img class="pt-2 pr-5" alt="Download on the App Store" src={appStoreBadge} width="90%" />
</a>
</div>
<div class="flex flex-col place-items-start">
<Text>F-Droid</Text>
<a href="https://f-droid.org/packages/app.alextran.immich/" target="_blank" id="fdroid-link">
<img class="w-[200px] mt-2" alt="Get it on F-Droid" src={fdroidBadge} />
</a> </a>
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { createApiKey, Permission } from '@immich/sdk'; import { createApiKey, Permission } from '@immich/sdk';
import { Button, Modal, ModalBody, obtainiumBadge, Text } from '@immich/ui'; import { Button, Modal, ModalBody, obtainiumBadge } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
let inputUrl = $state(location.origin); let inputUrl = $state(location.origin);
let inputApiKey = $state(''); let inputApiKey = $state('');
@@ -31,53 +31,64 @@
let { onClose }: Props = $props(); let { onClose }: Props = $props();
</script> </script>
<Modal title={$t('obtainium_configurator')} size="medium" {onClose}> <Modal title={$t('obtainium_configurator')} size="large" {onClose}>
<ModalBody> <ModalBody>
<div> <div class="flex flex-col sm:grid sm:grid-cols-2 gap-5 text-immich-primary dark:text-immich-dark-primary">
<Text color="muted" size="small"> <div>
{$t('obtainium_configurator_instructions')} <label
</Text> class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm"
<form class="mt-4"> for="obtainium-configurator"
<div class="mt-2"> >
<SettingInputField inputType={SettingInputFieldType.TEXT} label={$t('url')} bind:value={inputUrl} /> Obtainium
</label>
<div id="obtainium-configurator">
<form>
<div class="mt-2">
<SettingInputField inputType={SettingInputFieldType.TEXT} label={$t('url')} bind:value={inputUrl} />
</div>
<div class="mt-2">
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('api_key')}
bind:value={inputApiKey}
/>
</div>
<div class="">
<Button shape="round" size="small" onclick={() => handleCreate()}>{$t('new_api_key')}</Button>
</div>
<div class="mt-2">
<SettingSelect
label={$t('app_architecture_variant')}
bind:value={archVariant}
options={[
{ value: 'arm64-v8a-release', text: 'arm64-v8a' },
{ value: 'armeabi-v7a-release', text: 'armeabi-v7a' },
{ value: 'release', text: 'universal' },
{ value: 'x86_64-release', text: 'x86_64' },
]}
/>
</div>
</form>
</div> </div>
</div>
<div class="mt-2 flex gap-2 place-items-center place-content-center"> <div class="content-center">
<SettingInputField inputType={SettingInputFieldType.TEXT} label={$t('api_key')} bind:value={inputApiKey} /> {#if inputUrl && inputApiKey && archVariant}
<a
<div class="translate-y-[3px]"> href={obtainiumLink}
<Button size="small" onclick={() => handleCreate()}>{$t('create_api_key')}</Button> class="underline text-sm immich-form-label"
</div> target="_blank"
</div> rel="noreferrer"
id="obtainium-link"
<SettingSelect >
label={$t('app_architecture_variant')} <img class="pt-2 pr-5" alt="Get it on Obtainium" src={obtainiumBadge} />
bind:value={archVariant} </a>
options={[ {:else}
{ value: 'arm64-v8a-release', text: 'arm64-v8a' }, <p class="immich-form-label pb-2 text-sm" id="obtainium-link">
{ value: 'armeabi-v7a-release', text: 'armeabi-v7a' }, {$t('obtainium_configurator_instructions')}
{ value: 'release', text: 'universal' }, </p>
{ value: 'x86_64-release', text: 'x86_64' }, {/if}
]} </div>
/>
</form>
{#if inputUrl && inputApiKey && archVariant}
<div class="content-center">
<hr />
<div class="flex place-items-center place-content-center">
<a
href={obtainiumLink}
class="underline text-sm immich-form-label"
target="_blank"
rel="noreferrer"
id="obtainium-link"
>
<img class="pt-2 pr-5 h-[80px]" alt="Get it on Obtainium" src={obtainiumBadge} />
</a>
</div>
</div>
{/if}
</div> </div>
</ModalBody> </ModalBody>
</Modal> </Modal>

View File

@@ -1,48 +1,15 @@
import { retrieveServerConfig } from '$lib/stores/server-config.store'; import { retrieveServerConfig } from '$lib/stores/server-config.store';
import { AbortError, initLanguage, sleep } from '$lib/utils'; import { initLanguage } from '$lib/utils';
import { defaults } from '@immich/sdk'; import { defaults } from '@immich/sdk';
import { memoize } from 'lodash-es'; import { memoize } from 'lodash-es';
type Fetch = typeof fetch; type Fetch = typeof fetch;
const api_server: string = '@IMMICH_API_SERVER@';
const tryServers = async (fetchFn: typeof fetch) => {
const servers = api_server
.split(',')
.map((v) => v.trim())
.filter((v) => v !== '');
if (servers.length === 0) {
return true;
}
// servers are in priority order, try in parallel, use first success
const fetchers = servers.map(async (url: string) => {
const response = await fetchFn(url + '/server/config');
if (response.ok) {
return url;
}
throw new AbortError();
});
try {
const urlWinner = await Promise.any(fetchers);
defaults.baseUrl = urlWinner;
defaults.fetch = (url, options) => fetchFn(url, { credentials: 'include', ...options });
} catch (e) {
console.error(e);
return false;
}
};
async function _init(fetch: Fetch) { async function _init(fetch: Fetch) {
// set event.fetch on the fetch-client used by @immich/sdk // set event.fetch on the fetch-client used by @immich/sdk
// https://kit.svelte.dev/docs/load#making-fetch-requests // https://kit.svelte.dev/docs/load#making-fetch-requests
// https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options // https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options
defaults.fetch = fetch; defaults.fetch = fetch;
try {
await Promise.race([tryServers(fetch), sleep(5000)]);
} catch {
throw 'Could not connect to any server';
}
await initLanguage(); await initLanguage();
await retrieveServerConfig(); await retrieveServerConfig();
} }

View File

@@ -6,7 +6,6 @@
NotificationType, NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import DeviceCard from '$lib/components/user-settings-page/device-card.svelte';
import FeatureSetting from '$lib/components/users/FeatureSetting.svelte'; import FeatureSetting from '$lib/components/users/FeatureSetting.svelte';
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte'; import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte'; import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
@@ -37,7 +36,6 @@
} from '@immich/ui'; } from '@immich/ui';
import { import {
mdiAccountOutline, mdiAccountOutline,
mdiAppsBox,
mdiCameraIris, mdiCameraIris,
mdiChartPie, mdiChartPie,
mdiChartPieOutline, mdiChartPieOutline,
@@ -62,10 +60,11 @@
let user = $derived(data.user); let user = $derived(data.user);
const userPreferences = $derived(data.userPreferences); const userPreferences = $derived(data.userPreferences);
const userStatistics = $derived(data.userStatistics); const userStatistics = $derived(data.userStatistics);
const userSessions = $derived(data.userSessions);
const TiB = 1024 ** 4; const TiB = 1024 ** 4;
const usage = $derived(user.quotaUsageInBytes ?? 0); const usage = $derived(user.quotaUsageInBytes ?? 0);
let [statsUsage, statsUsageUnit] = $derived(getBytesWithUnit(usage, usage > TiB ? 2 : 0)); let [statsUsage, statsUsageUnit] = $derived(getBytesWithUnit(usage, usage > TiB ? 2 : 0));
const usedBytes = $derived(user.quotaUsageInBytes ?? 0); const usedBytes = $derived(user.quotaUsageInBytes ?? 0);
const availableBytes = $derived(user.quotaSizeInBytes ?? 1); const availableBytes = $derived(user.quotaSizeInBytes ?? 1);
let usedPercentage = $derived(Math.min(Math.round((usedBytes / availableBytes) * 100), 100)); let usedPercentage = $derived(Math.min(Math.round((usedBytes / availableBytes) * 100), 100));
@@ -351,25 +350,6 @@
{/if} {/if}
</CardBody> </CardBody>
</Card> </Card>
<Card color="secondary">
<CardHeader>
<div class="flex items-center gap-2 px-4 py-2 text-primary">
<Icon icon={mdiAppsBox} size="1.5rem" />
<CardTitle>Sessions</CardTitle>
</div>
</CardHeader>
<CardBody>
<div class="px-4 pb-7">
<Stack gap={3}>
{#each userSessions as session (session.id)}
<DeviceCard {session} />
{:else}
<span class="text-subtle">No mobile devices</span>
{/each}
</Stack>
</div>
</CardBody>
</Card>
</div> </div>
</Container> </Container>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { authenticate, requestServerInfo } from '$lib/utils/auth'; import { authenticate, requestServerInfo } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { getUserPreferencesAdmin, getUserSessionsAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk'; import { getUserPreferencesAdmin, getUserStatisticsAdmin, searchUsersAdmin } from '@immich/sdk';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
@@ -13,10 +13,9 @@ export const load = (async ({ params, url }) => {
redirect(302, AppRoute.ADMIN_USERS); redirect(302, AppRoute.ADMIN_USERS);
} }
const [userPreferences, userStatistics, userSessions] = await Promise.all([ const [userPreferences, userStatistics] = await Promise.all([
getUserPreferencesAdmin({ id: user.id }), getUserPreferencesAdmin({ id: user.id }),
getUserStatisticsAdmin({ id: user.id }), getUserStatisticsAdmin({ id: user.id }),
getUserSessionsAdmin({ id: user.id }),
]); ]);
const $t = await getFormatter(); const $t = await getFormatter();
@@ -25,7 +24,6 @@ export const load = (async ({ params, url }) => {
user, user,
userPreferences, userPreferences,
userStatistics, userStatistics,
userSessions,
meta: { meta: {
title: $t('admin.user_details'), title: $t('admin.user_details'),
}, },

View File

@@ -90,7 +90,7 @@
component: OnboardingMobileApp, component: OnboardingMobileApp,
role: OnboardingRole.USER, role: OnboardingRole.USER,
title: $t('mobile_app'), title: $t('mobile_app'),
icon: mdiCellphoneArrowDownVariant, icon: mdiCellphoneArrowDownVariant, // or you can use mdiCellphone
}, },
]); ]);
@@ -167,7 +167,7 @@
style="width: {(onboardingProgress / onboardingStepCount) * 100}%" style="width: {(onboardingProgress / onboardingStepCount) * 100}%"
></div> ></div>
</div> </div>
<div class="py-8 flex place-content-center place-items-center m-auto w-[min(100%,_800px)]"> <div class="py-8 flex place-content-center place-items-center m-auto">
<OnboardingCard <OnboardingCard
title={onboardingSteps[index].title} title={onboardingSteps[index].title}
icon={onboardingSteps[index].icon} icon={onboardingSteps[index].icon}

View File

@@ -1,4 +1,3 @@
import replace from '@rollup/plugin-replace';
import { enhancedImages } from '@sveltejs/enhanced-img'; import { enhancedImages } from '@sveltejs/enhanced-img';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
@@ -40,16 +39,6 @@ export default defineConfig({
enhancedImages(), enhancedImages(),
tailwindcss(), tailwindcss(),
sveltekit(), sveltekit(),
replace({
preventAssignment: true,
include: ['**/server.ts'],
sourceMap: true,
objectGuards: false,
delimiters: ['@', '@'],
values: {
IMMICH_API_SERVER: process.env.IMMICH_API_SERVER ?? '',
},
}),
process.env.BUILD_STATS === 'true' process.env.BUILD_STATS === 'true'
? visualizer({ ? visualizer({
emitFile: true, emitFile: true,