mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat: lock auth session (#18322)
This commit is contained in:
parent
ecb66fdb2c
commit
c1150fe7e3
37 changed files with 310 additions and 72 deletions
|
|
@ -1142,6 +1142,7 @@
|
||||||
"location_picker_latitude_hint": "Enter your latitude here",
|
"location_picker_latitude_hint": "Enter your latitude here",
|
||||||
"location_picker_longitude_error": "Enter a valid longitude",
|
"location_picker_longitude_error": "Enter a valid longitude",
|
||||||
"location_picker_longitude_hint": "Enter your longitude here",
|
"location_picker_longitude_hint": "Enter your longitude here",
|
||||||
|
"lock": "Lock",
|
||||||
"locked_folder": "Locked Folder",
|
"locked_folder": "Locked Folder",
|
||||||
"log_out": "Log out",
|
"log_out": "Log out",
|
||||||
"log_out_all_devices": "Log Out All Devices",
|
"log_out_all_devices": "Log Out All Devices",
|
||||||
|
|
|
||||||
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/authentication_api.dart
generated
BIN
mobile/openapi/lib/api/authentication_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/sessions_api.dart
generated
BIN
mobile/openapi/lib/api/sessions_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/auth_status_response_dto.dart
generated
BIN
mobile/openapi/lib/model/auth_status_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/permission.dart
generated
BIN
mobile/openapi/lib/model/permission.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/pin_code_reset_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/pin_code_reset_dto.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/session_response_dto.dart
generated
BIN
mobile/openapi/lib/model/session_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/session_unlock_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/session_unlock_dto.dart
generated
Normal file
Binary file not shown.
|
|
@ -2377,7 +2377,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/PinCodeChangeDto"
|
"$ref": "#/components/schemas/PinCodeResetDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -2470,15 +2470,40 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/pin-code/verify": {
|
"/auth/session/lock": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "verifyPinCode",
|
"operationId": "lockAuthSession",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/session/unlock": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "unlockAuthSession",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/PinCodeSetupDto"
|
"$ref": "#/components/schemas/SessionUnlockDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -5695,6 +5720,41 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/sessions/{id}/lock": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "lockSession",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Sessions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/shared-links": {
|
"/shared-links": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAllSharedLinks",
|
"operationId": "getAllSharedLinks",
|
||||||
|
|
@ -9327,6 +9387,9 @@
|
||||||
},
|
},
|
||||||
"AuthStatusResponseDto": {
|
"AuthStatusResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"isElevated": {
|
"isElevated": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
@ -9335,6 +9398,9 @@
|
||||||
},
|
},
|
||||||
"pinCode": {
|
"pinCode": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pinExpiresAt": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11096,6 +11162,7 @@
|
||||||
"session.read",
|
"session.read",
|
||||||
"session.update",
|
"session.update",
|
||||||
"session.delete",
|
"session.delete",
|
||||||
|
"session.lock",
|
||||||
"sharedLink.create",
|
"sharedLink.create",
|
||||||
"sharedLink.read",
|
"sharedLink.read",
|
||||||
"sharedLink.update",
|
"sharedLink.update",
|
||||||
|
|
@ -11297,6 +11364,18 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"PinCodeResetDto": {
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"example": "123456",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"PinCodeSetupDto": {
|
"PinCodeSetupDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"pinCode": {
|
"pinCode": {
|
||||||
|
|
@ -12109,6 +12188,9 @@
|
||||||
"deviceType": {
|
"deviceType": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -12144,6 +12226,9 @@
|
||||||
"deviceType": {
|
"deviceType": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -12161,6 +12246,18 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SessionUnlockDto": {
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"pinCode": {
|
||||||
|
"example": "123456",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SharedLinkCreateDto": {
|
"SharedLinkCreateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"albumId": {
|
"albumId": {
|
||||||
|
|
|
||||||
|
|
@ -512,18 +512,28 @@ export type LogoutResponseDto = {
|
||||||
redirectUri: string;
|
redirectUri: string;
|
||||||
successful: boolean;
|
successful: boolean;
|
||||||
};
|
};
|
||||||
export type PinCodeChangeDto = {
|
export type PinCodeResetDto = {
|
||||||
newPinCode: string;
|
|
||||||
password?: string;
|
password?: string;
|
||||||
pinCode?: string;
|
pinCode?: string;
|
||||||
};
|
};
|
||||||
export type PinCodeSetupDto = {
|
export type PinCodeSetupDto = {
|
||||||
pinCode: string;
|
pinCode: string;
|
||||||
};
|
};
|
||||||
|
export type PinCodeChangeDto = {
|
||||||
|
newPinCode: string;
|
||||||
|
password?: string;
|
||||||
|
pinCode?: string;
|
||||||
|
};
|
||||||
|
export type SessionUnlockDto = {
|
||||||
|
password?: string;
|
||||||
|
pinCode?: string;
|
||||||
|
};
|
||||||
export type AuthStatusResponseDto = {
|
export type AuthStatusResponseDto = {
|
||||||
|
expiresAt?: string;
|
||||||
isElevated: boolean;
|
isElevated: boolean;
|
||||||
password: boolean;
|
password: boolean;
|
||||||
pinCode: boolean;
|
pinCode: boolean;
|
||||||
|
pinExpiresAt?: string;
|
||||||
};
|
};
|
||||||
export type ValidateAccessTokenResponseDto = {
|
export type ValidateAccessTokenResponseDto = {
|
||||||
authStatus: boolean;
|
authStatus: boolean;
|
||||||
|
|
@ -1075,6 +1085,7 @@ export type SessionResponseDto = {
|
||||||
current: boolean;
|
current: boolean;
|
||||||
deviceOS: string;
|
deviceOS: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
|
expiresAt?: string;
|
||||||
id: string;
|
id: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
@ -1089,6 +1100,7 @@ export type SessionCreateResponseDto = {
|
||||||
current: boolean;
|
current: boolean;
|
||||||
deviceOS: string;
|
deviceOS: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
|
expiresAt?: string;
|
||||||
id: string;
|
id: string;
|
||||||
token: string;
|
token: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|
@ -2066,13 +2078,13 @@ export function logout(opts?: Oazapfts.RequestOpts) {
|
||||||
method: "POST"
|
method: "POST"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function resetPinCode({ pinCodeChangeDto }: {
|
export function resetPinCode({ pinCodeResetDto }: {
|
||||||
pinCodeChangeDto: PinCodeChangeDto;
|
pinCodeResetDto: PinCodeResetDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
|
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: pinCodeChangeDto
|
body: pinCodeResetDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function setupPinCode({ pinCodeSetupDto }: {
|
export function setupPinCode({ pinCodeSetupDto }: {
|
||||||
|
|
@ -2093,13 +2105,19 @@ export function changePinCode({ pinCodeChangeDto }: {
|
||||||
body: pinCodeChangeDto
|
body: pinCodeChangeDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function verifyPinCode({ pinCodeSetupDto }: {
|
export function lockAuthSession(opts?: Oazapfts.RequestOpts) {
|
||||||
pinCodeSetupDto: PinCodeSetupDto;
|
return oazapfts.ok(oazapfts.fetchText("/auth/session/lock", {
|
||||||
|
...opts,
|
||||||
|
method: "POST"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function unlockAuthSession({ sessionUnlockDto }: {
|
||||||
|
sessionUnlockDto: SessionUnlockDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchText("/auth/pin-code/verify", oazapfts.json({
|
return oazapfts.ok(oazapfts.fetchText("/auth/session/unlock", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: pinCodeSetupDto
|
body: sessionUnlockDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getAuthStatus(opts?: Oazapfts.RequestOpts) {
|
export function getAuthStatus(opts?: Oazapfts.RequestOpts) {
|
||||||
|
|
@ -2952,6 +2970,14 @@ export function deleteSession({ id }: {
|
||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function lockSession({ id }: {
|
||||||
|
id: string;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText(`/sessions/${encodeURIComponent(id)}/lock`, {
|
||||||
|
...opts,
|
||||||
|
method: "POST"
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function getAllSharedLinks({ albumId }: {
|
export function getAllSharedLinks({ albumId }: {
|
||||||
albumId?: string;
|
albumId?: string;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
|
@ -3709,6 +3735,7 @@ export enum Permission {
|
||||||
SessionRead = "session.read",
|
SessionRead = "session.read",
|
||||||
SessionUpdate = "session.update",
|
SessionUpdate = "session.update",
|
||||||
SessionDelete = "session.delete",
|
SessionDelete = "session.delete",
|
||||||
|
SessionLock = "session.lock",
|
||||||
SharedLinkCreate = "sharedLink.create",
|
SharedLinkCreate = "sharedLink.create",
|
||||||
SharedLinkRead = "sharedLink.read",
|
SharedLinkRead = "sharedLink.read",
|
||||||
SharedLinkUpdate = "sharedLink.update",
|
SharedLinkUpdate = "sharedLink.update",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import {
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
LogoutResponseDto,
|
LogoutResponseDto,
|
||||||
PinCodeChangeDto,
|
PinCodeChangeDto,
|
||||||
|
PinCodeResetDto,
|
||||||
PinCodeSetupDto,
|
PinCodeSetupDto,
|
||||||
|
SessionUnlockDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
ValidateAccessTokenResponseDto,
|
ValidateAccessTokenResponseDto,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
|
|
@ -98,14 +100,21 @@ export class AuthController {
|
||||||
|
|
||||||
@Delete('pin-code')
|
@Delete('pin-code')
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeChangeDto): Promise<void> {
|
async resetPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeResetDto): Promise<void> {
|
||||||
return this.service.resetPinCode(auth, dto);
|
return this.service.resetPinCode(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('pin-code/verify')
|
@Post('session/unlock')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Authenticated()
|
@Authenticated()
|
||||||
async verifyPinCode(@Auth() auth: AuthDto, @Body() dto: PinCodeSetupDto): Promise<void> {
|
async unlockAuthSession(@Auth() auth: AuthDto, @Body() dto: SessionUnlockDto): Promise<void> {
|
||||||
return this.service.verifyPinCode(auth, dto);
|
return this.service.unlockSession(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('session/lock')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Authenticated()
|
||||||
|
async lockAuthSession(@Auth() auth: AuthDto): Promise<void> {
|
||||||
|
return this.service.lockSession(auth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,11 @@ export class SessionController {
|
||||||
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
return this.service.delete(auth, id);
|
return this.service.delete(auth, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(':id/lock')
|
||||||
|
@Authenticated({ permission: Permission.SESSION_LOCK })
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
|
return this.service.lock(auth, id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,7 @@ export type Session = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
expiresAt: Date | null;
|
||||||
deviceOS: string;
|
deviceOS: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
pinExpiresAt: Date | null;
|
pinExpiresAt: Date | null;
|
||||||
|
|
|
||||||
2
server/src/db.d.ts
vendored
2
server/src/db.d.ts
vendored
|
|
@ -344,7 +344,7 @@ export interface Sessions {
|
||||||
deviceType: Generated<string>;
|
deviceType: Generated<string>;
|
||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
parentId: string | null;
|
parentId: string | null;
|
||||||
expiredAt: Date | null;
|
expiresAt: Date | null;
|
||||||
token: string;
|
token: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
updateId: Generated<string>;
|
updateId: Generated<string>;
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,8 @@ export class PinCodeResetDto {
|
||||||
password?: string;
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SessionUnlockDto extends PinCodeResetDto {}
|
||||||
|
|
||||||
export class PinCodeChangeDto extends PinCodeResetDto {
|
export class PinCodeChangeDto extends PinCodeResetDto {
|
||||||
@PinCode()
|
@PinCode()
|
||||||
newPinCode!: string;
|
newPinCode!: string;
|
||||||
|
|
@ -139,4 +141,6 @@ export class AuthStatusResponseDto {
|
||||||
pinCode!: boolean;
|
pinCode!: boolean;
|
||||||
password!: boolean;
|
password!: boolean;
|
||||||
isElevated!: boolean;
|
isElevated!: boolean;
|
||||||
|
expiresAt?: string;
|
||||||
|
pinExpiresAt?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export class SessionResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
createdAt!: string;
|
createdAt!: string;
|
||||||
updatedAt!: string;
|
updatedAt!: string;
|
||||||
|
expiresAt?: string;
|
||||||
current!: boolean;
|
current!: boolean;
|
||||||
deviceType!: string;
|
deviceType!: string;
|
||||||
deviceOS!: string;
|
deviceOS!: string;
|
||||||
|
|
@ -37,6 +38,7 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
createdAt: entity.createdAt.toISOString(),
|
createdAt: entity.createdAt.toISOString(),
|
||||||
updatedAt: entity.updatedAt.toISOString(),
|
updatedAt: entity.updatedAt.toISOString(),
|
||||||
|
expiresAt: entity.expiresAt?.toISOString(),
|
||||||
current: currentId === entity.id,
|
current: currentId === entity.id,
|
||||||
deviceOS: entity.deviceOS,
|
deviceOS: entity.deviceOS,
|
||||||
deviceType: entity.deviceType,
|
deviceType: entity.deviceType,
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,7 @@ export enum Permission {
|
||||||
SESSION_READ = 'session.read',
|
SESSION_READ = 'session.read',
|
||||||
SESSION_UPDATE = 'session.update',
|
SESSION_UPDATE = 'session.update',
|
||||||
SESSION_DELETE = 'session.delete',
|
SESSION_DELETE = 'session.delete',
|
||||||
|
SESSION_LOCK = 'session.lock',
|
||||||
|
|
||||||
SHARED_LINK_CREATE = 'sharedLink.create',
|
SHARED_LINK_CREATE = 'sharedLink.create',
|
||||||
SHARED_LINK_READ = 'sharedLink.read',
|
SHARED_LINK_READ = 'sharedLink.read',
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,15 @@ where
|
||||||
"partners"."sharedById" in ($1)
|
"partners"."sharedById" in ($1)
|
||||||
and "partners"."sharedWithId" = $2
|
and "partners"."sharedWithId" = $2
|
||||||
|
|
||||||
|
-- AccessRepository.session.checkOwnerAccess
|
||||||
|
select
|
||||||
|
"sessions"."id"
|
||||||
|
from
|
||||||
|
"sessions"
|
||||||
|
where
|
||||||
|
"sessions"."id" in ($1)
|
||||||
|
and "sessions"."userId" = $2
|
||||||
|
|
||||||
-- AccessRepository.stack.checkOwnerAccess
|
-- AccessRepository.stack.checkOwnerAccess
|
||||||
select
|
select
|
||||||
"stacks"."id"
|
"stacks"."id"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
-- NOTE: This file is auto generated by ./sql-generator
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
-- SessionRepository.search
|
-- SessionRepository.get
|
||||||
select
|
select
|
||||||
*
|
"id",
|
||||||
|
"expiresAt",
|
||||||
|
"pinExpiresAt"
|
||||||
from
|
from
|
||||||
"sessions"
|
"sessions"
|
||||||
where
|
where
|
||||||
"sessions"."updatedAt" <= $1
|
"id" = $1
|
||||||
|
|
||||||
-- SessionRepository.getByToken
|
-- SessionRepository.getByToken
|
||||||
select
|
select
|
||||||
|
|
@ -37,8 +39,8 @@ from
|
||||||
where
|
where
|
||||||
"sessions"."token" = $1
|
"sessions"."token" = $1
|
||||||
and (
|
and (
|
||||||
"sessions"."expiredAt" is null
|
"sessions"."expiresAt" is null
|
||||||
or "sessions"."expiredAt" > $2
|
or "sessions"."expiresAt" > $2
|
||||||
)
|
)
|
||||||
|
|
||||||
-- SessionRepository.getByUserId
|
-- SessionRepository.getByUserId
|
||||||
|
|
@ -50,6 +52,10 @@ from
|
||||||
and "users"."deletedAt" is null
|
and "users"."deletedAt" is null
|
||||||
where
|
where
|
||||||
"sessions"."userId" = $1
|
"sessions"."userId" = $1
|
||||||
|
and (
|
||||||
|
"sessions"."expiresAt" is null
|
||||||
|
or "sessions"."expiresAt" > $2
|
||||||
|
)
|
||||||
order by
|
order by
|
||||||
"sessions"."updatedAt" desc,
|
"sessions"."updatedAt" desc,
|
||||||
"sessions"."createdAt" desc
|
"sessions"."createdAt" desc
|
||||||
|
|
@ -58,3 +64,10 @@ order by
|
||||||
delete from "sessions"
|
delete from "sessions"
|
||||||
where
|
where
|
||||||
"id" = $1::uuid
|
"id" = $1::uuid
|
||||||
|
|
||||||
|
-- SessionRepository.lockAll
|
||||||
|
update "sessions"
|
||||||
|
set
|
||||||
|
"pinExpiresAt" = $1
|
||||||
|
where
|
||||||
|
"userId" = $2
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,25 @@ class NotificationAccess {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SessionAccess {
|
||||||
|
constructor(private db: Kysely<DB>) {}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||||
|
@ChunkedSet({ paramIndex: 1 })
|
||||||
|
async checkOwnerAccess(userId: string, sessionIds: Set<string>) {
|
||||||
|
if (sessionIds.size === 0) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.db
|
||||||
|
.selectFrom('sessions')
|
||||||
|
.select('sessions.id')
|
||||||
|
.where('sessions.id', 'in', [...sessionIds])
|
||||||
|
.where('sessions.userId', '=', userId)
|
||||||
|
.execute()
|
||||||
|
.then((sessions) => new Set(sessions.map((session) => session.id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
class StackAccess {
|
class StackAccess {
|
||||||
constructor(private db: Kysely<DB>) {}
|
constructor(private db: Kysely<DB>) {}
|
||||||
|
|
||||||
|
|
@ -456,6 +475,7 @@ export class AccessRepository {
|
||||||
notification: NotificationAccess;
|
notification: NotificationAccess;
|
||||||
person: PersonAccess;
|
person: PersonAccess;
|
||||||
partner: PartnerAccess;
|
partner: PartnerAccess;
|
||||||
|
session: SessionAccess;
|
||||||
stack: StackAccess;
|
stack: StackAccess;
|
||||||
tag: TagAccess;
|
tag: TagAccess;
|
||||||
timeline: TimelineAccess;
|
timeline: TimelineAccess;
|
||||||
|
|
@ -469,6 +489,7 @@ export class AccessRepository {
|
||||||
this.notification = new NotificationAccess(db);
|
this.notification = new NotificationAccess(db);
|
||||||
this.person = new PersonAccess(db);
|
this.person = new PersonAccess(db);
|
||||||
this.partner = new PartnerAccess(db);
|
this.partner = new PartnerAccess(db);
|
||||||
|
this.session = new SessionAccess(db);
|
||||||
this.stack = new StackAccess(db);
|
this.stack = new StackAccess(db);
|
||||||
this.tag = new TagAccess(db);
|
this.tag = new TagAccess(db);
|
||||||
this.timeline = new TimelineAccess(db);
|
this.timeline = new TimelineAccess(db);
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,20 @@ export class SessionRepository {
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([
|
eb.or([
|
||||||
eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()),
|
eb('updatedAt', '<=', DateTime.now().minus({ days: 90 }).toJSDate()),
|
||||||
eb.and([eb('expiredAt', 'is not', null), eb('expiredAt', '<=', DateTime.now().toJSDate())]),
|
eb.and([eb('expiresAt', 'is not', null), eb('expiresAt', '<=', DateTime.now().toJSDate())]),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.returning(['id', 'deviceOS', 'deviceType'])
|
.returning(['id', 'deviceOS', 'deviceType'])
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
search(options: SessionSearchOptions) {
|
get(id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('sessions')
|
.selectFrom('sessions')
|
||||||
.selectAll()
|
.select(['id', 'expiresAt', 'pinExpiresAt'])
|
||||||
.where('sessions.updatedAt', '<=', options.updatedBefore)
|
.where('id', '=', id)
|
||||||
.execute();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.STRING] })
|
||||||
|
|
@ -52,7 +52,7 @@ export class SessionRepository {
|
||||||
])
|
])
|
||||||
.where('sessions.token', '=', token)
|
.where('sessions.token', '=', token)
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb.or([eb('sessions.expiredAt', 'is', null), eb('sessions.expiredAt', '>', DateTime.now().toJSDate())]),
|
eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
|
||||||
)
|
)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
@ -64,6 +64,9 @@ export class SessionRepository {
|
||||||
.innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null))
|
.innerJoin('users', (join) => join.onRef('users.id', '=', 'sessions.userId').on('users.deletedAt', 'is', null))
|
||||||
.selectAll('sessions')
|
.selectAll('sessions')
|
||||||
.where('sessions.userId', '=', userId)
|
.where('sessions.userId', '=', userId)
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([eb('sessions.expiresAt', 'is', null), eb('sessions.expiresAt', '>', DateTime.now().toJSDate())]),
|
||||||
|
)
|
||||||
.orderBy('sessions.updatedAt', 'desc')
|
.orderBy('sessions.updatedAt', 'desc')
|
||||||
.orderBy('sessions.createdAt', 'desc')
|
.orderBy('sessions.createdAt', 'desc')
|
||||||
.execute();
|
.execute();
|
||||||
|
|
@ -86,4 +89,9 @@ export class SessionRepository {
|
||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
|
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
async lockAll(userId: string) {
|
||||||
|
await this.db.updateTable('sessions').set({ pinExpiresAt: null }).where('userId', '=', userId).execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "sessions" RENAME "expiredAt" TO "expiresAt";`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "sessions" RENAME "expiresAt" TO "expiredAt";`.execute(db);
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ export class SessionTable {
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
@Column({ type: 'timestamp with time zone', nullable: true })
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
expiredAt!: Date | null;
|
expiresAt!: Date | null;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
@ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
|
||||||
|
|
@ -924,13 +924,13 @@ describe(AuthService.name, () => {
|
||||||
const user = factory.userAdmin();
|
const user = factory.userAdmin();
|
||||||
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
mocks.user.getForPinCode.mockResolvedValue({ pinCode: '123456 (hashed)', password: '' });
|
||||||
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
mocks.crypto.compareBcrypt.mockImplementation((a, b) => `${a} (hashed)` === b);
|
||||||
mocks.session.getByUserId.mockResolvedValue([currentSession]);
|
mocks.session.lockAll.mockResolvedValue(void 0);
|
||||||
mocks.session.update.mockResolvedValue(currentSession);
|
mocks.session.update.mockResolvedValue(currentSession);
|
||||||
|
|
||||||
await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' });
|
await sut.resetPinCode(factory.auth({ user }), { pinCode: '123456' });
|
||||||
|
|
||||||
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
|
expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pinCode: null });
|
||||||
expect(mocks.session.update).toHaveBeenCalledWith(currentSession.id, { pinExpiresAt: null });
|
expect(mocks.session.lockAll).toHaveBeenCalledWith(user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if the PIN code does not match', async () => {
|
it('should throw if the PIN code does not match', async () => {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
PinCodeChangeDto,
|
PinCodeChangeDto,
|
||||||
PinCodeResetDto,
|
PinCodeResetDto,
|
||||||
PinCodeSetupDto,
|
PinCodeSetupDto,
|
||||||
|
SessionUnlockDto,
|
||||||
SignUpDto,
|
SignUpDto,
|
||||||
mapLoginResponse,
|
mapLoginResponse,
|
||||||
} from 'src/dtos/auth.dto';
|
} from 'src/dtos/auth.dto';
|
||||||
|
|
@ -123,24 +124,21 @@ export class AuthService extends BaseService {
|
||||||
|
|
||||||
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
|
async resetPinCode(auth: AuthDto, dto: PinCodeResetDto) {
|
||||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||||
this.resetPinChecks(user, dto);
|
this.validatePinCode(user, dto);
|
||||||
|
|
||||||
await this.userRepository.update(auth.user.id, { pinCode: null });
|
await this.userRepository.update(auth.user.id, { pinCode: null });
|
||||||
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
await this.sessionRepository.lockAll(auth.user.id);
|
||||||
for (const session of sessions) {
|
|
||||||
await this.sessionRepository.update(session.id, { pinExpiresAt: null });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
|
async changePinCode(auth: AuthDto, dto: PinCodeChangeDto) {
|
||||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||||
this.resetPinChecks(user, dto);
|
this.validatePinCode(user, dto);
|
||||||
|
|
||||||
const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
|
const hashed = await this.cryptoRepository.hashBcrypt(dto.newPinCode, SALT_ROUNDS);
|
||||||
await this.userRepository.update(auth.user.id, { pinCode: hashed });
|
await this.userRepository.update(auth.user.id, { pinCode: hashed });
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetPinChecks(
|
private validatePinCode(
|
||||||
user: { pinCode: string | null; password: string | null },
|
user: { pinCode: string | null; password: string | null },
|
||||||
dto: { pinCode?: string; password?: string },
|
dto: { pinCode?: string; password?: string },
|
||||||
) {
|
) {
|
||||||
|
|
@ -474,23 +472,27 @@ export class AuthService extends BaseService {
|
||||||
throw new UnauthorizedException('Invalid user token');
|
throw new UnauthorizedException('Invalid user token');
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyPinCode(auth: AuthDto, dto: PinCodeSetupDto): Promise<void> {
|
async unlockSession(auth: AuthDto, dto: SessionUnlockDto): Promise<void> {
|
||||||
const user = await this.userRepository.getForPinCode(auth.user.id);
|
|
||||||
if (!user) {
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resetPinChecks(user, { pinCode: dto.pinCode });
|
|
||||||
|
|
||||||
if (!auth.session) {
|
if (!auth.session) {
|
||||||
throw new BadRequestException('Session is missing');
|
throw new BadRequestException('This endpoint can only be used with a session token');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await this.userRepository.getForPinCode(auth.user.id);
|
||||||
|
this.validatePinCode(user, { pinCode: dto.pinCode });
|
||||||
|
|
||||||
await this.sessionRepository.update(auth.session.id, {
|
await this.sessionRepository.update(auth.session.id, {
|
||||||
pinExpiresAt: new Date(DateTime.now().plus({ minutes: 15 }).toJSDate()),
|
pinExpiresAt: DateTime.now().plus({ minutes: 15 }).toJSDate(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async lockSession(auth: AuthDto): Promise<void> {
|
||||||
|
if (!auth.session) {
|
||||||
|
throw new BadRequestException('This endpoint can only be used with a session token');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sessionRepository.update(auth.session.id, { pinExpiresAt: null });
|
||||||
|
}
|
||||||
|
|
||||||
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
|
private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) {
|
||||||
const token = this.cryptoRepository.randomBytesAsText(32);
|
const token = this.cryptoRepository.randomBytesAsText(32);
|
||||||
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
const tokenHashed = this.cryptoRepository.hashSha256(token);
|
||||||
|
|
@ -526,10 +528,14 @@ export class AuthService extends BaseService {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const session = auth.session ? await this.sessionRepository.get(auth.session.id) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pinCode: !!user.pinCode,
|
pinCode: !!user.pinCode,
|
||||||
password: !!user.password,
|
password: !!user.password,
|
||||||
isElevated: !!auth.session?.hasElevatedPermission,
|
isElevated: !!auth.session?.hasElevatedPermission,
|
||||||
|
expiresAt: session?.expiresAt?.toISOString(),
|
||||||
|
pinExpiresAt: session?.pinExpiresAt?.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export class SessionService extends BaseService {
|
||||||
const session = await this.sessionRepository.create({
|
const session = await this.sessionRepository.create({
|
||||||
parentId: auth.session.id,
|
parentId: auth.session.id,
|
||||||
userId: auth.user.id,
|
userId: auth.user.id,
|
||||||
expiredAt: 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,
|
||||||
|
|
@ -49,6 +49,11 @@ export class SessionService extends BaseService {
|
||||||
await this.sessionRepository.delete(id);
|
await this.sessionRepository.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async lock(auth: AuthDto, id: string): Promise<void> {
|
||||||
|
await this.requireAccess({ auth, permission: Permission.SESSION_LOCK, ids: [id] });
|
||||||
|
await this.sessionRepository.update(id, { pinExpiresAt: null });
|
||||||
|
}
|
||||||
|
|
||||||
async deleteAll(auth: AuthDto): Promise<void> {
|
async deleteAll(auth: AuthDto): Promise<void> {
|
||||||
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
const sessions = await this.sessionRepository.getByUserId(auth.user.id);
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,13 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
||||||
return await access.partner.checkUpdateAccess(auth.user.id, ids);
|
return await access.partner.checkUpdateAccess(auth.user.id, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case Permission.SESSION_READ:
|
||||||
|
case Permission.SESSION_UPDATE:
|
||||||
|
case Permission.SESSION_DELETE:
|
||||||
|
case Permission.SESSION_LOCK: {
|
||||||
|
return access.session.checkOwnerAccess(auth.user.id, ids);
|
||||||
|
}
|
||||||
|
|
||||||
case Permission.STACK_READ: {
|
case Permission.STACK_READ: {
|
||||||
return access.stack.checkOwnerAccess(auth.user.id, ids);
|
return access.stack.checkOwnerAccess(auth.user.id, ids);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
||||||
checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
session: {
|
||||||
|
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
|
},
|
||||||
|
|
||||||
stack: {
|
stack: {
|
||||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
|
||||||
deviceType: 'mobile',
|
deviceType: 'mobile',
|
||||||
token: 'abc123',
|
token: 'abc123',
|
||||||
parentId: null,
|
parentId: null,
|
||||||
expiredAt: null,
|
expiresAt: null,
|
||||||
userId: newUuid(),
|
userId: newUuid(),
|
||||||
pinExpiresAt: newDate(),
|
pinExpiresAt: newDate(),
|
||||||
...session,
|
...session,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||||
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
|
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
|
||||||
|
|
@ -10,11 +11,12 @@
|
||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility, lockAuthSession } from '@immich/sdk';
|
||||||
import { mdiDotsVertical } from '@mdi/js';
|
import { Button } from '@immich/ui';
|
||||||
|
import { mdiDotsVertical, mdiLockOutline } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
@ -42,6 +44,11 @@
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
assetStore.removeAssets(assetIds);
|
assetStore.removeAssets(assetIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLock = async () => {
|
||||||
|
await lockAuthSession();
|
||||||
|
await goto(AppRoute.PHOTOS);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Multi-selection mode app bar -->
|
<!-- Multi-selection mode app bar -->
|
||||||
|
|
@ -62,6 +69,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
|
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
|
||||||
|
{#snippet buttons()}
|
||||||
|
<Button size="small" variant="filled" color="warning" leadingIcon={mdiLockOutline} onclick={handleLock}>
|
||||||
|
{$t('lock')}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<AssetGrid
|
<AssetGrid
|
||||||
enableRouting={true}
|
enableRouting={true}
|
||||||
{assetStore}
|
{assetStore}
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,12 @@ import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
|
|
||||||
const { isElevated, pinCode } = await getAuthStatus();
|
const { isElevated, pinCode } = await getAuthStatus();
|
||||||
|
|
||||||
if (!isElevated || !pinCode) {
|
if (!isElevated || !pinCode) {
|
||||||
const continuePath = encodeURIComponent(url.pathname);
|
redirect(302, `${AppRoute.AUTH_PIN_PROMPT}?continue=${encodeURIComponent(url.pathname + url.search)}`);
|
||||||
const redirectPath = `${AppRoute.AUTH_PIN_PROMPT}?continue=${continuePath}`;
|
|
||||||
|
|
||||||
redirect(302, redirectPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const asset = await getAssetInfoFromParam(params);
|
const asset = await getAssetInfoFromParam(params);
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
|
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
|
||||||
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
|
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
|
||||||
import PincodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
import PincodeInput from '$lib/components/user-settings-page/PinCodeInput.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { verifyPinCode } from '@immich/sdk';
|
import { unlockAuthSession } from '@immich/sdk';
|
||||||
import { Icon } from '@immich/ui';
|
import { Icon } from '@immich/ui';
|
||||||
import { mdiLockOpenVariantOutline, mdiLockOutline, mdiLockSmart } from '@mdi/js';
|
import { mdiLockOpenVariantOutline, mdiLockOutline, mdiLockSmart } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
@ -23,17 +22,15 @@
|
||||||
let hasPinCode = $derived(data.hasPinCode);
|
let hasPinCode = $derived(data.hasPinCode);
|
||||||
let pinCode = $state('');
|
let pinCode = $state('');
|
||||||
|
|
||||||
const onPinFilled = async (code: string, withDelay = false) => {
|
const handleUnlockSession = async (code: string) => {
|
||||||
try {
|
try {
|
||||||
await verifyPinCode({ pinCodeSetupDto: { pinCode: code } });
|
await unlockAuthSession({ sessionUnlockDto: { pinCode: code } });
|
||||||
|
|
||||||
isVerified = true;
|
isVerified = true;
|
||||||
|
|
||||||
if (withDelay) {
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
void goto(data.continuePath ?? AppRoute.LOCKED);
|
await goto(data.continueUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('wrong_pin_code'));
|
handleError(error, $t('wrong_pin_code'));
|
||||||
isBadPinCode = true;
|
isBadPinCode = true;
|
||||||
|
|
@ -64,7 +61,7 @@
|
||||||
bind:value={pinCode}
|
bind:value={pinCode}
|
||||||
tabindexStart={1}
|
tabindexStart={1}
|
||||||
pinLength={6}
|
pinLength={6}
|
||||||
onFilled={(pinCode) => onPinFilled(pinCode, true)}
|
onFilled={handleUnlockSession}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAuthStatus } from '@immich/sdk';
|
import { getAuthStatus } from '@immich/sdk';
|
||||||
|
|
@ -8,8 +9,6 @@ export const load = (async ({ url }) => {
|
||||||
|
|
||||||
const { pinCode } = await getAuthStatus();
|
const { pinCode } = await getAuthStatus();
|
||||||
|
|
||||||
const continuePath = url.searchParams.get('continue');
|
|
||||||
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -17,6 +16,6 @@ export const load = (async ({ url }) => {
|
||||||
title: $t('pin_verification'),
|
title: $t('pin_verification'),
|
||||||
},
|
},
|
||||||
hasPinCode: !!pinCode,
|
hasPinCode: !!pinCode,
|
||||||
continuePath,
|
continueUrl: url.searchParams.get('continue') || AppRoute.LOCKED,
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
}) satisfies PageLoad;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue