From 72106ad74d019cea159641707506684d0aa18d2b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:48:42 +0200 Subject: [PATCH] Add expiration date to access --- apps/api/src/app/access/access.service.ts | 29 +++++++++++++++ .../app/endpoints/public/public.service.ts | 4 ++- .../impersonation/impersonation.module.ts | 8 ++++- .../impersonation.service.spec.ts | 2 ++ .../impersonation/impersonation.service.ts | 35 ++++--------------- .../access-table/access-table.component.ts | 4 +-- .../components/header/header.component.html | 4 +-- .../app/components/header/header.component.ts | 8 +++++ ...reate-or-update-access-dialog.component.ts | 5 ++- .../src/lib/interfaces/access.interface.ts | 2 +- 10 files changed, 62 insertions(+), 39 deletions(-) diff --git a/apps/api/src/app/access/access.service.ts b/apps/api/src/app/access/access.service.ts index e50a6c7d0..d033677fe 100644 --- a/apps/api/src/app/access/access.service.ts +++ b/apps/api/src/app/access/access.service.ts @@ -4,6 +4,7 @@ import { AccessWithGranteeUser } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; import { Access, Prisma } from '@prisma/client'; +import { isBefore, isToday } from 'date-fns'; @Injectable() export class AccessService { @@ -60,6 +61,10 @@ export class AccessService { }); } + public isExpired({ expiresAt }: Pick) { + return expiresAt ? isBefore(expiresAt, new Date()) : false; + } + public async updateAccess({ data, where @@ -72,4 +77,28 @@ export class AccessService { where }); } + + /** + * Stores the date of the last usage of the access. The value is the first + * usage of the day, because a request which repeats must not write to the + * database again. + */ + public async updateLastUsedAt({ + id, + lastUsedAt + }: Pick) { + if (lastUsedAt && isToday(lastUsedAt)) { + return; + } + + try { + await this.prismaService.access.update({ + data: { lastUsedAt: new Date() }, + where: { id } + }); + } catch { + // The date of the last usage is not essential for the request, hence a + // failure to store it must not fail the request + } + } } diff --git a/apps/api/src/app/endpoints/public/public.service.ts b/apps/api/src/app/endpoints/public/public.service.ts index 277cc0d8c..0b709cd01 100644 --- a/apps/api/src/app/endpoints/public/public.service.ts +++ b/apps/api/src/app/endpoints/public/public.service.ts @@ -40,13 +40,15 @@ export class PublicService { type: 'PUBLIC' }); - if (!access) { + if (!access || this.accessService.isExpired(access)) { throw new HttpException( getReasonPhrase(StatusCodes.NOT_FOUND), StatusCodes.NOT_FOUND ); } + await this.accessService.updateLastUsedAt(access); + let hasDetails = true; const user = await this.userService.user({ diff --git a/apps/api/src/services/impersonation/impersonation.module.ts b/apps/api/src/services/impersonation/impersonation.module.ts index ff15af11a..b0e0567fa 100644 --- a/apps/api/src/services/impersonation/impersonation.module.ts +++ b/apps/api/src/services/impersonation/impersonation.module.ts @@ -1,3 +1,4 @@ +import { AccessModule } from '@ghostfolio/api/app/access/access.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; @@ -6,7 +7,12 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { Module } from '@nestjs/common'; @Module({ - imports: [ConfigurationModule, PrismaModule, SubscriptionModule], + imports: [ + AccessModule, + ConfigurationModule, + PrismaModule, + SubscriptionModule + ], providers: [ImpersonationService], exports: [ImpersonationService] }) diff --git a/apps/api/src/services/impersonation/impersonation.service.spec.ts b/apps/api/src/services/impersonation/impersonation.service.spec.ts index 60c5d3492..3cfb90b4f 100644 --- a/apps/api/src/services/impersonation/impersonation.service.spec.ts +++ b/apps/api/src/services/impersonation/impersonation.service.spec.ts @@ -1,3 +1,4 @@ +import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; @@ -100,6 +101,7 @@ describe('Impersonation service', () => { getSubscription, updateAccess, service: new ImpersonationService( + new AccessService(prismaService), configurationService, prismaService, subscriptionService diff --git a/apps/api/src/services/impersonation/impersonation.service.ts b/apps/api/src/services/impersonation/impersonation.service.ts index 3c27f907e..2a7915aa5 100644 --- a/apps/api/src/services/impersonation/impersonation.service.ts +++ b/apps/api/src/services/impersonation/impersonation.service.ts @@ -1,3 +1,4 @@ +import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; @@ -16,11 +17,11 @@ import type { import { Injectable } from '@nestjs/common'; import { Access, AccessType } from '@prisma/client'; -import { isBefore, isToday } from 'date-fns'; @Injectable() export class ImpersonationService { public constructor( + private readonly accessService: AccessService, private readonly configurationService: ConfigurationService, private readonly prismaService: PrismaService, private readonly subscriptionService: SubscriptionService @@ -113,8 +114,8 @@ export class ImpersonationService { } }); - if (accessObject?.userId && !isExpired(accessObject)) { - await this.recordUsage(accessObject); + if (accessObject?.userId && !this.accessService.isExpired(accessObject)) { + await this.accessService.updateLastUsedAt(accessObject); return { access: accessObject, userId: accessObject.userId }; } else if ( @@ -137,8 +138,8 @@ export class ImpersonationService { } }); - if (accessObject?.userId && !isExpired(accessObject)) { - await this.recordUsage(accessObject); + if (accessObject?.userId && !this.accessService.isExpired(accessObject)) { + await this.accessService.updateLastUsedAt(accessObject); return { access: accessObject, userId: accessObject.userId }; } @@ -146,28 +147,4 @@ export class ImpersonationService { return { userId: null }; } - - /** - * Records that the access has been used. The value is the first usage of the - * day, because a request which repeats must not write to the database again. - */ - private async recordUsage({ id, lastUsedAt }: Access) { - if (lastUsedAt && isToday(lastUsedAt)) { - return; - } - - try { - await this.prismaService.access.update({ - data: { lastUsedAt: new Date() }, - where: { id } - }); - } catch { - // The date of the last usage is not essential for the request, hence a - // failure to store it must not fail the request - } - } -} - -function isExpired({ expiresAt }: Access) { - return expiresAt ? isBefore(expiresAt, new Date()) : false; } diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index fa43a77e0..72c1066ac 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -85,9 +85,9 @@ export class GfAccessTableComponent { 'alias', 'grantee', 'type', - 'details', 'lastUsedAt', - 'expiresAt' + 'expiresAt', + 'details' ]; if (this.showActions()) { diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index eaccbb332..0806765f7 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -203,7 +203,7 @@ >
} - @if (user()?.access?.length > 0) { + @if (accesses()?.length > 0) { - @for (accessItem of user()?.access; track accessItem.id) { + @for (accessItem of accesses(); track accessItem.id) {