From 2573936d7f5d2c366117e8504f0177903d6ff01a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 7 Oct 2025 14:25:18 -0400 Subject: [PATCH] feat: session permissions --- mobile/lib/utils/openapi_patching.dart | 11 ++++ .../openapi/lib/model/login_response_dto.dart | 10 +++- .../model/session_create_response_dto.dart | 10 +++- .../lib/model/session_response_dto.dart | 10 +++- open-api/immich-openapi-specs.json | 21 ++++++++ open-api/typescript-sdk/src/fetch-client.ts | 3 ++ server/src/database.ts | 4 +- server/src/dtos/auth.dto.ts | 26 ++------- server/src/dtos/session.dto.ts | 6 ++- server/src/queries/session.repository.sql | 4 +- server/src/repositories/session.repository.ts | 2 +- .../1759858637977-AddSessionPermissions.ts | 11 ++++ server/src/schema/tables/session.table.ts | 4 ++ server/src/services/auth.service.spec.ts | 2 + server/src/services/auth.service.ts | 53 ++++++++++++++----- server/src/services/session.service.ts | 8 ++- server/test/small.factory.ts | 1 + 17 files changed, 143 insertions(+), 43 deletions(-) create mode 100644 server/src/schema/migrations/1759858637977-AddSessionPermissions.ts diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 33199d5225..fc967aa064 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -39,6 +39,17 @@ dynamic upgradeDto(dynamic value, String targetType) { case 'LoginResponseDto': if (value is Map) { 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; case 'SyncUserV1': diff --git a/mobile/openapi/lib/model/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart index 82a4f9b3ed..70e50f803f 100644 --- a/mobile/openapi/lib/model/login_response_dto.dart +++ b/mobile/openapi/lib/model/login_response_dto.dart @@ -17,6 +17,7 @@ class LoginResponseDto { required this.isAdmin, required this.isOnboarded, required this.name, + this.permissions = const [], required this.profileImagePath, required this.shouldChangePassword, required this.userEmail, @@ -31,6 +32,8 @@ class LoginResponseDto { String name; + List permissions; + String profileImagePath; bool shouldChangePassword; @@ -45,6 +48,7 @@ class LoginResponseDto { other.isAdmin == isAdmin && other.isOnboarded == isOnboarded && other.name == name && + _deepEquality.equals(other.permissions, permissions) && other.profileImagePath == profileImagePath && other.shouldChangePassword == shouldChangePassword && other.userEmail == userEmail && @@ -57,13 +61,14 @@ class LoginResponseDto { (isAdmin.hashCode) + (isOnboarded.hashCode) + (name.hashCode) + + (permissions.hashCode) + (profileImagePath.hashCode) + (shouldChangePassword.hashCode) + (userEmail.hashCode) + (userId.hashCode); @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 toJson() { final json = {}; @@ -71,6 +76,7 @@ class LoginResponseDto { json[r'isAdmin'] = this.isAdmin; json[r'isOnboarded'] = this.isOnboarded; json[r'name'] = this.name; + json[r'permissions'] = this.permissions; json[r'profileImagePath'] = this.profileImagePath; json[r'shouldChangePassword'] = this.shouldChangePassword; json[r'userEmail'] = this.userEmail; @@ -91,6 +97,7 @@ class LoginResponseDto { isAdmin: mapValueOfType(json, r'isAdmin')!, isOnboarded: mapValueOfType(json, r'isOnboarded')!, name: mapValueOfType(json, r'name')!, + permissions: Permission.listFromJson(json[r'permissions']), profileImagePath: mapValueOfType(json, r'profileImagePath')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, userEmail: mapValueOfType(json, r'userEmail')!, @@ -146,6 +153,7 @@ class LoginResponseDto { 'isAdmin', 'isOnboarded', 'name', + 'permissions', 'profileImagePath', 'shouldChangePassword', 'userEmail', diff --git a/mobile/openapi/lib/model/session_create_response_dto.dart b/mobile/openapi/lib/model/session_create_response_dto.dart index a4f93e8d9c..078cbf5ca7 100644 --- a/mobile/openapi/lib/model/session_create_response_dto.dart +++ b/mobile/openapi/lib/model/session_create_response_dto.dart @@ -20,6 +20,7 @@ class SessionCreateResponseDto { this.expiresAt, required this.id, required this.isPendingSyncReset, + this.permissions = const [], required this.token, required this.updatedAt, }); @@ -44,6 +45,8 @@ class SessionCreateResponseDto { bool isPendingSyncReset; + List permissions; + String token; String updatedAt; @@ -57,6 +60,7 @@ class SessionCreateResponseDto { other.expiresAt == expiresAt && other.id == id && other.isPendingSyncReset == isPendingSyncReset && + _deepEquality.equals(other.permissions, permissions) && other.token == token && other.updatedAt == updatedAt; @@ -70,11 +74,12 @@ class SessionCreateResponseDto { (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + (isPendingSyncReset.hashCode) + + (permissions.hashCode) + (token.hashCode) + (updatedAt.hashCode); @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 toJson() { final json = {}; @@ -89,6 +94,7 @@ class SessionCreateResponseDto { } json[r'id'] = this.id; json[r'isPendingSyncReset'] = this.isPendingSyncReset; + json[r'permissions'] = this.permissions; json[r'token'] = this.token; json[r'updatedAt'] = this.updatedAt; return json; @@ -110,6 +116,7 @@ class SessionCreateResponseDto { expiresAt: mapValueOfType(json, r'expiresAt'), id: mapValueOfType(json, r'id')!, isPendingSyncReset: mapValueOfType(json, r'isPendingSyncReset')!, + permissions: Permission.listFromJson(json[r'permissions']), token: mapValueOfType(json, r'token')!, updatedAt: mapValueOfType(json, r'updatedAt')!, ); @@ -165,6 +172,7 @@ class SessionCreateResponseDto { 'deviceType', 'id', 'isPendingSyncReset', + 'permissions', 'token', 'updatedAt', }; diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart index e76e4d48b4..885ba09b3a 100644 --- a/mobile/openapi/lib/model/session_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -20,6 +20,7 @@ class SessionResponseDto { this.expiresAt, required this.id, required this.isPendingSyncReset, + this.permissions = const [], required this.updatedAt, }); @@ -43,6 +44,8 @@ class SessionResponseDto { bool isPendingSyncReset; + List permissions; + String updatedAt; @override @@ -54,6 +57,7 @@ class SessionResponseDto { other.expiresAt == expiresAt && other.id == id && other.isPendingSyncReset == isPendingSyncReset && + _deepEquality.equals(other.permissions, permissions) && other.updatedAt == updatedAt; @override @@ -66,10 +70,11 @@ class SessionResponseDto { (expiresAt == null ? 0 : expiresAt!.hashCode) + (id.hashCode) + (isPendingSyncReset.hashCode) + + (permissions.hashCode) + (updatedAt.hashCode); @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 toJson() { final json = {}; @@ -84,6 +89,7 @@ class SessionResponseDto { } json[r'id'] = this.id; json[r'isPendingSyncReset'] = this.isPendingSyncReset; + json[r'permissions'] = this.permissions; json[r'updatedAt'] = this.updatedAt; return json; } @@ -104,6 +110,7 @@ class SessionResponseDto { expiresAt: mapValueOfType(json, r'expiresAt'), id: mapValueOfType(json, r'id')!, isPendingSyncReset: mapValueOfType(json, r'isPendingSyncReset')!, + permissions: Permission.listFromJson(json[r'permissions']), updatedAt: mapValueOfType(json, r'updatedAt')!, ); } @@ -158,6 +165,7 @@ class SessionResponseDto { 'deviceType', 'id', 'isPendingSyncReset', + 'permissions', 'updatedAt', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b574bc6624..215bd3d794 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -12219,6 +12219,12 @@ "name": { "type": "string" }, + "permissions": { + "items": { + "$ref": "#/components/schemas/Permission" + }, + "type": "array" + }, "profileImagePath": { "type": "string" }, @@ -12237,6 +12243,7 @@ "isAdmin", "isOnboarded", "name", + "permissions", "profileImagePath", "shouldChangePassword", "userEmail", @@ -14301,6 +14308,12 @@ "isPendingSyncReset": { "type": "boolean" }, + "permissions": { + "items": { + "$ref": "#/components/schemas/Permission" + }, + "type": "array" + }, "token": { "type": "string" }, @@ -14315,6 +14328,7 @@ "deviceType", "id", "isPendingSyncReset", + "permissions", "token", "updatedAt" ], @@ -14343,6 +14357,12 @@ "isPendingSyncReset": { "type": "boolean" }, + "permissions": { + "items": { + "$ref": "#/components/schemas/Permission" + }, + "type": "array" + }, "updatedAt": { "type": "string" } @@ -14354,6 +14374,7 @@ "deviceType", "id", "isPendingSyncReset", + "permissions", "updatedAt" ], "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index c8a69dfe8c..47748fdccd 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -556,6 +556,7 @@ export type LoginResponseDto = { isAdmin: boolean; isOnboarded: boolean; name: string; + permissions: Permission[]; profileImagePath: string; shouldChangePassword: boolean; userEmail: string; @@ -1194,6 +1195,7 @@ export type SessionResponseDto = { expiresAt?: string; id: string; isPendingSyncReset: boolean; + permissions: Permission[]; updatedAt: string; }; export type SessionCreateDto = { @@ -1210,6 +1212,7 @@ export type SessionCreateResponseDto = { expiresAt?: string; id: string; isPendingSyncReset: boolean; + permissions: Permission[]; token: string; updatedAt: string; }; diff --git a/server/src/database.ts b/server/src/database.ts index f472c643ee..d26993e75d 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -203,6 +203,7 @@ export type Album = Selectable & { export type AuthSession = { id: string; hasElevatedPermission: boolean; + permissions: Permission[]; }; export type Partner = { @@ -240,6 +241,7 @@ export type Session = { deviceType: string; pinExpiresAt: Date | null; isPendingSyncReset: boolean; + permissions: Permission[]; }; export type Exif = Omit, 'updatedAt' | 'updateId'>; @@ -308,7 +310,7 @@ export const columns = { assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'], authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], authApiKey: ['api_key.id', 'api_key.permissions'], - authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt'], + authSession: ['session.id', 'session.updatedAt', 'session.pinExpiresAt', 'session.permissions'], authSharedLink: [ 'shared_link.id', 'shared_link.userId', diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 2bb98b34a5..e90bc76803 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -1,10 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; -import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; -import { ImmichCookie, UserMetadataKey } from 'src/enum'; -import { UserMetadataItem } from 'src/types'; -import { Optional, PinCode, toEmail } from 'src/validation'; +import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser } from 'src/database'; +import { ImmichCookie, Permission } from 'src/enum'; +import { Optional, PinCode, toEmail, ValidateEnum } from 'src/validation'; export type CookieResponse = { isSecure: boolean; @@ -41,23 +40,8 @@ export class LoginResponseDto { isAdmin!: boolean; shouldChangePassword!: boolean; isOnboarded!: boolean; -} - -export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { - const onboardingMetadata = entity.metadata.find( - (item): item is UserMetadataItem => 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, - }; + @ValidateEnum({ enum: Permission, name: 'Permission', each: true }) + permissions!: Permission[]; } export class LogoutResponseDto { diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index 7ccc72a5f1..38bcb3bf59 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -1,6 +1,7 @@ import { Equals, IsInt, IsPositive, IsString } from 'class-validator'; 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 { /** @@ -35,6 +36,8 @@ export class SessionResponseDto { deviceType!: string; deviceOS!: string; isPendingSyncReset!: boolean; + @ValidateEnum({ enum: Permission, name: 'Permission', each: true }) + permissions!: Permission[]; } export class SessionCreateResponseDto extends SessionResponseDto { @@ -50,4 +53,5 @@ export const mapSession = (entity: Session, currentId?: string): SessionResponse deviceOS: entity.deviceOS, deviceType: entity.deviceType, isPendingSyncReset: entity.isPendingSyncReset, + permissions: entity.permissions, }); diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 34d25cce8a..9724deffb6 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -4,7 +4,8 @@ select "id", "expiresAt", - "pinExpiresAt" + "pinExpiresAt", + "permissions" from "session" where @@ -23,6 +24,7 @@ select "session"."id", "session"."updatedAt", "session"."pinExpiresAt", + "session"."permissions", ( select to_json(obj) diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index cdc0ab12db..a1a8ff6a07 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -32,7 +32,7 @@ export class SessionRepository { get(id: string) { return this.db .selectFrom('session') - .select(['id', 'expiresAt', 'pinExpiresAt']) + .select(['id', 'expiresAt', 'pinExpiresAt', 'permissions']) .where('id', '=', id) .executeTakeFirst(); } diff --git a/server/src/schema/migrations/1759858637977-AddSessionPermissions.ts b/server/src/schema/migrations/1759858637977-AddSessionPermissions.ts new file mode 100644 index 0000000000..08b9a2364e --- /dev/null +++ b/server/src/schema/migrations/1759858637977-AddSessionPermissions.ts @@ -0,0 +1,11 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + 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): Promise { + await sql`ALTER TABLE "session" DROP COLUMN "permissions";`.execute(db); +} diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index 706abdf887..085095fc7f 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -1,4 +1,5 @@ import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { Permission } from 'src/enum'; import { UserTable } from 'src/schema/tables/user.table'; import { Column, @@ -50,4 +51,7 @@ export class SessionTable { @Column({ type: 'timestamp with time zone', nullable: true }) pinExpiresAt!: Timestamp | null; + + @Column({ array: true, type: 'character varying' }) + permissions!: Permission[]; } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index d2b287cd5e..536f4d6e37 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -30,6 +30,7 @@ const oauthResponse = ({ isAdmin: false, isOnboarded: false, shouldChangePassword: false, + permissions: [Permission.All], }); // const token = Buffer.from('my-api-key', 'utf8').toString('base64'); @@ -104,6 +105,7 @@ describe(AuthService.name, () => { isAdmin: user.isAdmin, isOnboarded: false, shouldChangePassword: user.shouldChangePassword, + permissions: [Permission.All], }); expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 535df779cd..6556abfa5a 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -12,6 +12,7 @@ import { AuthStatusResponseDto, ChangePasswordDto, LoginCredentialDto, + LoginResponseDto, LogoutResponseDto, OAuthCallbackDto, OAuthConfigDto, @@ -20,12 +21,21 @@ import { PinCodeSetupDto, SessionUnlockDto, SignUpDto, - mapLoginResponse, } from 'src/dtos/auth.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 { BaseService } from 'src/services/base.service'; +import { UserMetadataItem } from 'src/types'; import { isGranted } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; import { mimeTypes } from 'src/utils/mime-types'; @@ -75,7 +85,7 @@ export class AuthService extends BaseService { 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 { @@ -177,6 +187,7 @@ export class AuthService extends BaseService { const authDto = await this.validate({ headers, queryParams }); const { adminRoute, sharedLinkRoute, uri } = metadata; const requestedPermission = metadata.permission ?? Permission.All; + const currentPermissions = authDto.apiKey?.permissions || authDto.session?.permissions; if (!authDto.user.isAdmin && adminRoute) { this.logger.warn(`Denied access to admin only route: ${uri}`); @@ -189,9 +200,9 @@ export class AuthService extends BaseService { } if ( - authDto.apiKey && requestedPermission !== false && - !isGranted({ requested: [requestedPermission], current: authDto.apiKey.permissions }) + currentPermissions && + !isGranted({ requested: [requestedPermission], current: currentPermissions }) ) { throw new ForbiddenException(`Missing required permission: ${requestedPermission}`); } @@ -322,7 +333,7 @@ export class AuthService extends BaseService { 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) { @@ -492,6 +503,7 @@ export class AuthService extends BaseService { user: session.user, session: { id: session.id, + permissions: session.permissions, hasElevatedPermission, }, }; @@ -521,18 +533,31 @@ export class AuthService extends BaseService { 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 { const token = this.cryptoRepository.randomBytesAsText(32); const tokenHashed = this.cryptoRepository.hashSha256(token); - await this.sessionRepository.create({ - token: tokenHashed, - deviceOS: loginDetails.deviceOS, - deviceType: loginDetails.deviceType, - userId: user.id, - }); + await this.sessionRepository.create({ token: tokenHashed, deviceOS, deviceType, userId: user.id, permissions }); - return mapLoginResponse(user, token); + const onboardingMetadata = user.metadata.find( + (item): item is UserMetadataItem => 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(profile: OAuthProfile, options: ClaimOptions): T { diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index a9c7e92fcb..a35d3bfddf 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -31,15 +31,21 @@ export class SessionService extends BaseService { 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 tokenHashed = this.cryptoRepository.hashSha256(token); const session = await this.sessionRepository.create({ - parentId: auth.session.id, + parentId: parent.id, userId: auth.user.id, expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null, deviceType: dto.deviceType, deviceOS: dto.deviceOS, token: tokenHashed, + permissions: parent.permissions, }); return { ...mapSession(session), token }; diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 04654552a3..10393a2514 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -135,6 +135,7 @@ const sessionFactory = (session: Partial = {}) => ({ userId: newUuid(), pinExpiresAt: newDate(), isPendingSyncReset: false, + permissions: [Permission.All], ...session, });