mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
Merge 2573936d7f into 9d639607c7
This commit is contained in:
commit
2282edf9ff
17 changed files with 143 additions and 43 deletions
|
|
@ -39,6 +39,17 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||||
case 'LoginResponseDto':
|
case 'LoginResponseDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'isOnboarded', false);
|
addDefault(value, 'isOnboarded', false);
|
||||||
|
addDefault(value, 'permissions', ['all']);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'SessionResponseDto':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(value, 'permissions', ['all']);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'SessionCreateResponseDto':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(value, 'permissions', ['all']);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'SyncUserV1':
|
case 'SyncUserV1':
|
||||||
|
|
|
||||||
10
mobile/openapi/lib/model/login_response_dto.dart
generated
10
mobile/openapi/lib/model/login_response_dto.dart
generated
|
|
@ -17,6 +17,7 @@ class LoginResponseDto {
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
required this.isOnboarded,
|
required this.isOnboarded,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
this.permissions = const [],
|
||||||
required this.profileImagePath,
|
required this.profileImagePath,
|
||||||
required this.shouldChangePassword,
|
required this.shouldChangePassword,
|
||||||
required this.userEmail,
|
required this.userEmail,
|
||||||
|
|
@ -31,6 +32,8 @@ class LoginResponseDto {
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
|
List<Permission> permissions;
|
||||||
|
|
||||||
String profileImagePath;
|
String profileImagePath;
|
||||||
|
|
||||||
bool shouldChangePassword;
|
bool shouldChangePassword;
|
||||||
|
|
@ -45,6 +48,7 @@ class LoginResponseDto {
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
other.isOnboarded == isOnboarded &&
|
other.isOnboarded == isOnboarded &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
|
_deepEquality.equals(other.permissions, permissions) &&
|
||||||
other.profileImagePath == profileImagePath &&
|
other.profileImagePath == profileImagePath &&
|
||||||
other.shouldChangePassword == shouldChangePassword &&
|
other.shouldChangePassword == shouldChangePassword &&
|
||||||
other.userEmail == userEmail &&
|
other.userEmail == userEmail &&
|
||||||
|
|
@ -57,13 +61,14 @@ class LoginResponseDto {
|
||||||
(isAdmin.hashCode) +
|
(isAdmin.hashCode) +
|
||||||
(isOnboarded.hashCode) +
|
(isOnboarded.hashCode) +
|
||||||
(name.hashCode) +
|
(name.hashCode) +
|
||||||
|
(permissions.hashCode) +
|
||||||
(profileImagePath.hashCode) +
|
(profileImagePath.hashCode) +
|
||||||
(shouldChangePassword.hashCode) +
|
(shouldChangePassword.hashCode) +
|
||||||
(userEmail.hashCode) +
|
(userEmail.hashCode) +
|
||||||
(userId.hashCode);
|
(userId.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, isOnboarded=$isOnboarded, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]';
|
String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, isOnboarded=$isOnboarded, name=$name, permissions=$permissions, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
|
@ -71,6 +76,7 @@ class LoginResponseDto {
|
||||||
json[r'isAdmin'] = this.isAdmin;
|
json[r'isAdmin'] = this.isAdmin;
|
||||||
json[r'isOnboarded'] = this.isOnboarded;
|
json[r'isOnboarded'] = this.isOnboarded;
|
||||||
json[r'name'] = this.name;
|
json[r'name'] = this.name;
|
||||||
|
json[r'permissions'] = this.permissions;
|
||||||
json[r'profileImagePath'] = this.profileImagePath;
|
json[r'profileImagePath'] = this.profileImagePath;
|
||||||
json[r'shouldChangePassword'] = this.shouldChangePassword;
|
json[r'shouldChangePassword'] = this.shouldChangePassword;
|
||||||
json[r'userEmail'] = this.userEmail;
|
json[r'userEmail'] = this.userEmail;
|
||||||
|
|
@ -91,6 +97,7 @@ class LoginResponseDto {
|
||||||
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
isAdmin: mapValueOfType<bool>(json, r'isAdmin')!,
|
||||||
isOnboarded: mapValueOfType<bool>(json, r'isOnboarded')!,
|
isOnboarded: mapValueOfType<bool>(json, r'isOnboarded')!,
|
||||||
name: mapValueOfType<String>(json, r'name')!,
|
name: mapValueOfType<String>(json, r'name')!,
|
||||||
|
permissions: Permission.listFromJson(json[r'permissions']),
|
||||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
||||||
userEmail: mapValueOfType<String>(json, r'userEmail')!,
|
userEmail: mapValueOfType<String>(json, r'userEmail')!,
|
||||||
|
|
@ -146,6 +153,7 @@ class LoginResponseDto {
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
'isOnboarded',
|
'isOnboarded',
|
||||||
'name',
|
'name',
|
||||||
|
'permissions',
|
||||||
'profileImagePath',
|
'profileImagePath',
|
||||||
'shouldChangePassword',
|
'shouldChangePassword',
|
||||||
'userEmail',
|
'userEmail',
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class SessionCreateResponseDto {
|
||||||
this.expiresAt,
|
this.expiresAt,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.isPendingSyncReset,
|
required this.isPendingSyncReset,
|
||||||
|
this.permissions = const [],
|
||||||
required this.token,
|
required this.token,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
@ -44,6 +45,8 @@ class SessionCreateResponseDto {
|
||||||
|
|
||||||
bool isPendingSyncReset;
|
bool isPendingSyncReset;
|
||||||
|
|
||||||
|
List<Permission> permissions;
|
||||||
|
|
||||||
String token;
|
String token;
|
||||||
|
|
||||||
String updatedAt;
|
String updatedAt;
|
||||||
|
|
@ -57,6 +60,7 @@ class SessionCreateResponseDto {
|
||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isPendingSyncReset == isPendingSyncReset &&
|
other.isPendingSyncReset == isPendingSyncReset &&
|
||||||
|
_deepEquality.equals(other.permissions, permissions) &&
|
||||||
other.token == token &&
|
other.token == token &&
|
||||||
other.updatedAt == updatedAt;
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
|
|
@ -70,11 +74,12 @@ class SessionCreateResponseDto {
|
||||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isPendingSyncReset.hashCode) +
|
(isPendingSyncReset.hashCode) +
|
||||||
|
(permissions.hashCode) +
|
||||||
(token.hashCode) +
|
(token.hashCode) +
|
||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SessionCreateResponseDto[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, permissions=$permissions, token=$token, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
|
@ -89,6 +94,7 @@ class SessionCreateResponseDto {
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
||||||
|
json[r'permissions'] = this.permissions;
|
||||||
json[r'token'] = this.token;
|
json[r'token'] = this.token;
|
||||||
json[r'updatedAt'] = this.updatedAt;
|
json[r'updatedAt'] = this.updatedAt;
|
||||||
return json;
|
return json;
|
||||||
|
|
@ -110,6 +116,7 @@ class SessionCreateResponseDto {
|
||||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
|
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
|
||||||
|
permissions: Permission.listFromJson(json[r'permissions']),
|
||||||
token: mapValueOfType<String>(json, r'token')!,
|
token: mapValueOfType<String>(json, r'token')!,
|
||||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||||
);
|
);
|
||||||
|
|
@ -165,6 +172,7 @@ class SessionCreateResponseDto {
|
||||||
'deviceType',
|
'deviceType',
|
||||||
'id',
|
'id',
|
||||||
'isPendingSyncReset',
|
'isPendingSyncReset',
|
||||||
|
'permissions',
|
||||||
'token',
|
'token',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
10
mobile/openapi/lib/model/session_response_dto.dart
generated
10
mobile/openapi/lib/model/session_response_dto.dart
generated
|
|
@ -20,6 +20,7 @@ class SessionResponseDto {
|
||||||
this.expiresAt,
|
this.expiresAt,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.isPendingSyncReset,
|
required this.isPendingSyncReset,
|
||||||
|
this.permissions = const [],
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -43,6 +44,8 @@ class SessionResponseDto {
|
||||||
|
|
||||||
bool isPendingSyncReset;
|
bool isPendingSyncReset;
|
||||||
|
|
||||||
|
List<Permission> permissions;
|
||||||
|
|
||||||
String updatedAt;
|
String updatedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -54,6 +57,7 @@ class SessionResponseDto {
|
||||||
other.expiresAt == expiresAt &&
|
other.expiresAt == expiresAt &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isPendingSyncReset == isPendingSyncReset &&
|
other.isPendingSyncReset == isPendingSyncReset &&
|
||||||
|
_deepEquality.equals(other.permissions, permissions) &&
|
||||||
other.updatedAt == updatedAt;
|
other.updatedAt == updatedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -66,10 +70,11 @@ class SessionResponseDto {
|
||||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isPendingSyncReset.hashCode) +
|
(isPendingSyncReset.hashCode) +
|
||||||
|
(permissions.hashCode) +
|
||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SessionResponseDto[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, permissions=$permissions, updatedAt=$updatedAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
|
@ -84,6 +89,7 @@ class SessionResponseDto {
|
||||||
}
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
json[r'isPendingSyncReset'] = this.isPendingSyncReset;
|
||||||
|
json[r'permissions'] = this.permissions;
|
||||||
json[r'updatedAt'] = this.updatedAt;
|
json[r'updatedAt'] = this.updatedAt;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +110,7 @@ class SessionResponseDto {
|
||||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
|
isPendingSyncReset: mapValueOfType<bool>(json, r'isPendingSyncReset')!,
|
||||||
|
permissions: Permission.listFromJson(json[r'permissions']),
|
||||||
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
updatedAt: mapValueOfType<String>(json, r'updatedAt')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +165,7 @@ class SessionResponseDto {
|
||||||
'deviceType',
|
'deviceType',
|
||||||
'id',
|
'id',
|
||||||
'isPendingSyncReset',
|
'isPendingSyncReset',
|
||||||
|
'permissions',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12240,6 +12240,12 @@
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"permissions": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Permission"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"profileImagePath": {
|
"profileImagePath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -12258,6 +12264,7 @@
|
||||||
"isAdmin",
|
"isAdmin",
|
||||||
"isOnboarded",
|
"isOnboarded",
|
||||||
"name",
|
"name",
|
||||||
|
"permissions",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"shouldChangePassword",
|
"shouldChangePassword",
|
||||||
"userEmail",
|
"userEmail",
|
||||||
|
|
@ -14324,6 +14331,12 @@
|
||||||
"isPendingSyncReset": {
|
"isPendingSyncReset": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"permissions": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Permission"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"token": {
|
"token": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -14338,6 +14351,7 @@
|
||||||
"deviceType",
|
"deviceType",
|
||||||
"id",
|
"id",
|
||||||
"isPendingSyncReset",
|
"isPendingSyncReset",
|
||||||
|
"permissions",
|
||||||
"token",
|
"token",
|
||||||
"updatedAt"
|
"updatedAt"
|
||||||
],
|
],
|
||||||
|
|
@ -14366,6 +14380,12 @@
|
||||||
"isPendingSyncReset": {
|
"isPendingSyncReset": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"permissions": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Permission"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
"updatedAt": {
|
"updatedAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
@ -14377,6 +14397,7 @@
|
||||||
"deviceType",
|
"deviceType",
|
||||||
"id",
|
"id",
|
||||||
"isPendingSyncReset",
|
"isPendingSyncReset",
|
||||||
|
"permissions",
|
||||||
"updatedAt"
|
"updatedAt"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
|
||||||
|
|
@ -561,6 +561,7 @@ export type LoginResponseDto = {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isOnboarded: boolean;
|
isOnboarded: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
|
permissions: Permission[];
|
||||||
profileImagePath: string;
|
profileImagePath: string;
|
||||||
shouldChangePassword: boolean;
|
shouldChangePassword: boolean;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
|
|
@ -1199,6 +1200,7 @@ export type SessionResponseDto = {
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
id: string;
|
id: string;
|
||||||
isPendingSyncReset: boolean;
|
isPendingSyncReset: boolean;
|
||||||
|
permissions: Permission[];
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
export type SessionCreateDto = {
|
export type SessionCreateDto = {
|
||||||
|
|
@ -1215,6 +1217,7 @@ export type SessionCreateResponseDto = {
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
id: string;
|
id: string;
|
||||||
isPendingSyncReset: boolean;
|
isPendingSyncReset: boolean;
|
||||||
|
permissions: Permission[];
|
||||||
token: string;
|
token: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,7 @@ export type Album = Selectable<AlbumTable> & {
|
||||||
export type AuthSession = {
|
export type AuthSession = {
|
||||||
id: string;
|
id: string;
|
||||||
hasElevatedPermission: boolean;
|
hasElevatedPermission: boolean;
|
||||||
|
permissions: Permission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Partner = {
|
export type Partner = {
|
||||||
|
|
@ -240,6 +241,7 @@ export type Session = {
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
pinExpiresAt: Date | null;
|
pinExpiresAt: Date | null;
|
||||||
isPendingSyncReset: boolean;
|
isPendingSyncReset: boolean;
|
||||||
|
permissions: Permission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Exif = Omit<Selectable<AssetExifTable>, 'updatedAt' | 'updateId'>;
|
export type Exif = Omit<Selectable<AssetExifTable>, 'updatedAt' | 'updateId'>;
|
||||||
|
|
@ -308,7 +310,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'],
|
authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.permissions'],
|
||||||
authSharedLink: [
|
authSharedLink: [
|
||||||
'shared_link.id',
|
'shared_link.id',
|
||||||
'shared_link.userId',
|
'shared_link.userId',
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
|
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser } from 'src/database';
|
||||||
import { ImmichCookie, UserMetadataKey } from 'src/enum';
|
import { ImmichCookie, Permission } from 'src/enum';
|
||||||
import { UserMetadataItem } from 'src/types';
|
import { Optional, PinCode, toEmail, ValidateEnum } from 'src/validation';
|
||||||
import { Optional, PinCode, toEmail } from 'src/validation';
|
|
||||||
|
|
||||||
export type CookieResponse = {
|
export type CookieResponse = {
|
||||||
isSecure: boolean;
|
isSecure: boolean;
|
||||||
|
|
@ -41,23 +40,8 @@ export class LoginResponseDto {
|
||||||
isAdmin!: boolean;
|
isAdmin!: boolean;
|
||||||
shouldChangePassword!: boolean;
|
shouldChangePassword!: boolean;
|
||||||
isOnboarded!: boolean;
|
isOnboarded!: boolean;
|
||||||
}
|
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
|
||||||
|
permissions!: Permission[];
|
||||||
export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto {
|
|
||||||
const onboardingMetadata = entity.metadata.find(
|
|
||||||
(item): item is UserMetadataItem<UserMetadataKey.Onboarding> => item.key === UserMetadataKey.Onboarding,
|
|
||||||
)?.value;
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessToken,
|
|
||||||
userId: entity.id,
|
|
||||||
userEmail: entity.email,
|
|
||||||
name: entity.name,
|
|
||||||
isAdmin: entity.isAdmin,
|
|
||||||
profileImagePath: entity.profileImagePath,
|
|
||||||
shouldChangePassword: entity.shouldChangePassword,
|
|
||||||
isOnboarded: onboardingMetadata?.isOnboarded ?? false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LogoutResponseDto {
|
export class LogoutResponseDto {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Equals, IsInt, IsPositive, IsString } from 'class-validator';
|
import { Equals, IsInt, IsPositive, IsString } from 'class-validator';
|
||||||
import { Session } from 'src/database';
|
import { Session } from 'src/database';
|
||||||
import { Optional, ValidateBoolean } from 'src/validation';
|
import { Permission } from 'src/enum';
|
||||||
|
import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
|
||||||
|
|
||||||
export class SessionCreateDto {
|
export class SessionCreateDto {
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,6 +36,8 @@ export class SessionResponseDto {
|
||||||
deviceType!: string;
|
deviceType!: string;
|
||||||
deviceOS!: string;
|
deviceOS!: string;
|
||||||
isPendingSyncReset!: boolean;
|
isPendingSyncReset!: boolean;
|
||||||
|
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
|
||||||
|
permissions!: Permission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SessionCreateResponseDto extends SessionResponseDto {
|
export class SessionCreateResponseDto extends SessionResponseDto {
|
||||||
|
|
@ -50,4 +53,5 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse
|
||||||
deviceOS: entity.deviceOS,
|
deviceOS: entity.deviceOS,
|
||||||
deviceType: entity.deviceType,
|
deviceType: entity.deviceType,
|
||||||
isPendingSyncReset: entity.isPendingSyncReset,
|
isPendingSyncReset: entity.isPendingSyncReset,
|
||||||
|
permissions: entity.permissions,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
select
|
select
|
||||||
"id",
|
"id",
|
||||||
"expiresAt",
|
"expiresAt",
|
||||||
"pinExpiresAt"
|
"pinExpiresAt",
|
||||||
|
"permissions"
|
||||||
from
|
from
|
||||||
"session"
|
"session"
|
||||||
where
|
where
|
||||||
|
|
@ -23,6 +24,7 @@ select
|
||||||
"session"."id",
|
"session"."id",
|
||||||
"session"."updatedAt",
|
"session"."updatedAt",
|
||||||
"session"."pinExpiresAt",
|
"session"."pinExpiresAt",
|
||||||
|
"session"."permissions",
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
to_json(obj)
|
to_json(obj)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export class SessionRepository {
|
||||||
get(id: string) {
|
get(id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('session')
|
.selectFrom('session')
|
||||||
.select(['id', 'expiresAt', 'pinExpiresAt'])
|
.select(['id', 'expiresAt', 'pinExpiresAt', 'permissions'])
|
||||||
.where('id', '=', id)
|
.where('id', '=', id)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "session" ADD "permissions" character varying[];`.execute(db);
|
||||||
|
await sql`UPDATE "session" SET "permissions" = ARRAY['all'];`.execute(db);
|
||||||
|
await sql`ALTER TABLE "session" ALTER COLUMN "permissions" SET NOT NULL`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "session" DROP COLUMN "permissions";`.execute(db);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
|
import { Permission } from 'src/enum';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
|
|
@ -50,4 +51,7 @@ export class SessionTable {
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
pinExpiresAt!: Timestamp | null;
|
pinExpiresAt!: Timestamp | null;
|
||||||
|
|
||||||
|
@Column({ array: true, type: 'character varying' })
|
||||||
|
permissions!: Permission[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ const oauthResponse = ({
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
|
permissions: [Permission.All],
|
||||||
});
|
});
|
||||||
|
|
||||||
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
|
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
|
||||||
|
|
@ -104,6 +105,7 @@ describe(AuthService.name, () => {
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isOnboarded: false,
|
isOnboarded: false,
|
||||||
shouldChangePassword: user.shouldChangePassword,
|
shouldChangePassword: user.shouldChangePassword,
|
||||||
|
permissions: [Permission.All],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
AuthStatusResponseDto,
|
AuthStatusResponseDto,
|
||||||
ChangePasswordDto,
|
ChangePasswordDto,
|
||||||
LoginCredentialDto,
|
LoginCredentialDto,
|
||||||
|
LoginResponseDto,
|
||||||
LogoutResponseDto,
|
LogoutResponseDto,
|
||||||
OAuthCallbackDto,
|
OAuthCallbackDto,
|
||||||
OAuthConfigDto,
|
OAuthConfigDto,
|
||||||
|
|
@ -20,12 +21,21 @@ import {
|
||||||
PinCodeSetupDto,
|
PinCodeSetupDto,
|
||||||
SessionUnlockDto,
|
SessionUnlockDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
mapLoginResponse,
|
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, JobName, Permission, StorageFolder } from 'src/enum';
|
import {
|
||||||
|
AuthType,
|
||||||
|
ImmichCookie,
|
||||||
|
ImmichHeader,
|
||||||
|
ImmichQuery,
|
||||||
|
JobName,
|
||||||
|
Permission,
|
||||||
|
StorageFolder,
|
||||||
|
UserMetadataKey,
|
||||||
|
} from 'src/enum';
|
||||||
import { OAuthProfile } from 'src/repositories/oauth.repository';
|
import { OAuthProfile } from 'src/repositories/oauth.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { UserMetadataItem } from 'src/types';
|
||||||
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';
|
||||||
|
|
@ -75,7 +85,7 @@ export class AuthService extends BaseService {
|
||||||
throw new UnauthorizedException('Incorrect email or password');
|
throw new UnauthorizedException('Incorrect email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.createLoginResponse(user, details);
|
return this.createLoginResponse(user, details, [Permission.All]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
||||||
|
|
@ -177,6 +187,7 @@ export class AuthService extends BaseService {
|
||||||
const authDto = await this.validate({ headers, queryParams });
|
const authDto = await this.validate({ headers, queryParams });
|
||||||
const { adminRoute, sharedLinkRoute, uri } = metadata;
|
const { adminRoute, sharedLinkRoute, uri } = metadata;
|
||||||
const requestedPermission = metadata.permission ?? Permission.All;
|
const requestedPermission = metadata.permission ?? Permission.All;
|
||||||
|
const currentPermissions = authDto.apiKey?.permissions || authDto.session?.permissions;
|
||||||
|
|
||||||
if (!authDto.user.isAdmin && adminRoute) {
|
if (!authDto.user.isAdmin && adminRoute) {
|
||||||
this.logger.warn(`Denied access to admin only route: ${uri}`);
|
this.logger.warn(`Denied access to admin only route: ${uri}`);
|
||||||
|
|
@ -189,9 +200,9 @@ export class AuthService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
authDto.apiKey &&
|
|
||||||
requestedPermission !== false &&
|
requestedPermission !== false &&
|
||||||
!isGranted({ requested: [requestedPermission], current: authDto.apiKey.permissions })
|
currentPermissions &&
|
||||||
|
!isGranted({ requested: [requestedPermission], current: currentPermissions })
|
||||||
) {
|
) {
|
||||||
throw new ForbiddenException(`Missing required permission: ${requestedPermission}`);
|
throw new ForbiddenException(`Missing required permission: ${requestedPermission}`);
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +333,7 @@ export class AuthService extends BaseService {
|
||||||
await this.syncProfilePicture(user, profile.picture);
|
await this.syncProfilePicture(user, profile.picture);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.createLoginResponse(user, loginDetails);
|
return this.createLoginResponse(user, loginDetails, [Permission.All]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncProfilePicture(user: UserAdmin, url: string) {
|
private async syncProfilePicture(user: UserAdmin, url: string) {
|
||||||
|
|
@ -492,6 +503,7 @@ export class AuthService extends BaseService {
|
||||||
user: session.user,
|
user: session.user,
|
||||||
session: {
|
session: {
|
||||||
id: session.id,
|
id: session.id,
|
||||||
|
permissions: session.permissions,
|
||||||
hasElevatedPermission,
|
hasElevatedPermission,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -521,18 +533,31 @@ export class AuthService extends BaseService {
|
||||||
await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
|
await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
|
private async createLoginResponse(
|
||||||
|
user: UserAdmin,
|
||||||
|
{ deviceOS, deviceType }: LoginDetails,
|
||||||
|
permissions: Permission[],
|
||||||
|
): Promise<LoginResponseDto> {
|
||||||
const token = this.cryptoRepository.randomBytesAsText(32);
|
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||||
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
||||||
|
|
||||||
await this.sessionRepository.create({
|
await this.sessionRepository.create({ token: tokenHashed, deviceOS, deviceType, userId: user.id, permissions });
|
||||||
token: tokenHashed,
|
|
||||||
deviceOS: loginDetails.deviceOS,
|
|
||||||
deviceType: loginDetails.deviceType,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapLoginResponse(user, token);
|
const onboardingMetadata = user.metadata.find(
|
||||||
|
(item): item is UserMetadataItem<UserMetadataKey.Onboarding> => item.key === UserMetadataKey.Onboarding,
|
||||||
|
)?.value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: token,
|
||||||
|
userId: user.id,
|
||||||
|
userEmail: user.email,
|
||||||
|
name: user.name,
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
profileImagePath: user.profileImagePath,
|
||||||
|
shouldChangePassword: user.shouldChangePassword,
|
||||||
|
isOnboarded: onboardingMetadata?.isOnboarded ?? false,
|
||||||
|
permissions,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
||||||
|
|
|
||||||
|
|
@ -31,15 +31,21 @@ export class SessionService extends BaseService {
|
||||||
throw new BadRequestException('This endpoint can only be used with a session token');
|
throw new BadRequestException('This endpoint can only be used with a session token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parent = await this.sessionRepository.get(auth.session.id);
|
||||||
|
if (!parent) {
|
||||||
|
throw new BadRequestException('Session not found');
|
||||||
|
}
|
||||||
|
|
||||||
const token = this.cryptoRepository.randomBytesAsText(32);
|
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||||
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
||||||
const session = await this.sessionRepository.create({
|
const session = await this.sessionRepository.create({
|
||||||
parentId: auth.session.id,
|
parentId: parent.id,
|
||||||
userId: auth.user.id,
|
userId: auth.user.id,
|
||||||
expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
|
expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null,
|
||||||
deviceType: dto.deviceType,
|
deviceType: dto.deviceType,
|
||||||
deviceOS: dto.deviceOS,
|
deviceOS: dto.deviceOS,
|
||||||
token: tokenHashed,
|
token: tokenHashed,
|
||||||
|
permissions: parent.permissions,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...mapSession(session), token };
|
return { ...mapSession(session), token };
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
|
||||||
userId: newUuid(),
|
userId: newUuid(),
|
||||||
pinExpiresAt: newDate(),
|
pinExpiresAt: newDate(),
|
||||||
isPendingSyncReset: false,
|
isPendingSyncReset: false,
|
||||||
|
permissions: [Permission.All],
|
||||||
...session,
|
...session,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue