| Details |
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 c73d088f8..fa43a77e0 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
@@ -1,5 +1,6 @@
import { MCP_ENDPOINT } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
+import { getDateFormatString, getLocale } from '@ghostfolio/common/helper';
import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes';
@@ -9,6 +10,7 @@ import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services';
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
+import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@@ -41,6 +43,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ClipboardModule,
+ CommonModule,
GfAccessLevelIconComponent,
IonIcon,
MatButtonModule,
@@ -78,7 +81,14 @@ export class GfAccessTableComponent {
protected readonly dataSource = new MatTableDataSource();
protected readonly displayedColumns = computed(() => {
- const columns = ['alias', 'grantee', 'type', 'details'];
+ const columns = [
+ 'alias',
+ 'grantee',
+ 'type',
+ 'details',
+ 'lastUsedAt',
+ 'expiresAt'
+ ];
if (this.showActions()) {
columns.push('actions');
@@ -87,6 +97,7 @@ export class GfAccessTableComponent {
return columns;
});
+ protected readonly defaultDateFormat = getDateFormatString(getLocale());
protected readonly getAccessLevel = getAccessLevel;
protected hasPermissionToEnableMcp = false;
diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
index 4054de639..d7d921cdb 100644
--- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
+++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts
@@ -1,5 +1,6 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
+import { getToday } from '@ghostfolio/common/helper';
import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import {
@@ -41,6 +42,7 @@ import {
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
+import { MatDatepickerModule } from '@angular/material/datepicker';
import {
MAT_DIALOG_DATA,
MatDialogModule,
@@ -49,6 +51,7 @@ import {
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
+import { addYears, endOfDay } from 'date-fns';
import { StatusCodes } from 'http-status-codes';
import { EMPTY, catchError } from 'rxjs';
@@ -63,6 +66,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
GfPortfolioFilterFormComponent,
MatButtonModule,
MatDialogModule,
+ MatDatepickerModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
@@ -80,6 +84,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
protected accessForm: FormGroup;
protected readonly mode: 'create' | 'update';
+ protected readonly today = getToday();
private hasExperimentalFeatures = false;
private hasPermissionToEnableMcp = false;
@@ -129,6 +134,12 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
this.accessForm = this.formBuilder.group({
accessLevel: getAccessLevel(access?.scopes),
alias: [access?.alias ?? ''],
+ expiresAt: [
+ access?.expiresAt
+ ? new Date(access.expiresAt)
+ : addYears(this.today, 1),
+ Validators.required
+ ],
filters: [null],
granteeUserId: [
isPrivate ? (access?.grantee ?? null) : null,
@@ -203,6 +214,12 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
}
}
+ private buildExpiresAt() {
+ const expiresAt = this.accessForm.get('expiresAt')?.value as Date;
+
+ return endOfDay(expiresAt).toISOString();
+ }
+
private buildFilters(): Filter[] {
return getFiltersFromPortfolioFilterFormValue(
this.accessForm.get('filters')?.value
@@ -229,6 +246,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
const access: CreateAccessDto = {
alias: this.accessForm.get('alias')?.value,
+ expiresAt: this.buildExpiresAt(),
filters: filters.length > 0 ? filters : undefined,
granteeUserId: this.accessForm.get('granteeUserId')?.value,
scopes: this.buildScopes(),
@@ -288,6 +306,7 @@ export class GfCreateOrUpdateAccessDialogComponent implements OnInit {
const access: UpdateAccessDto = {
alias: this.accessForm.get('alias')?.value,
+ expiresAt: this.buildExpiresAt(),
filters: filters.length > 0 ? filters : undefined,
granteeUserId: this.accessForm.get('granteeUserId')?.value,
id: accessId,
diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
index 131e4d782..3aabec69f 100644
--- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
+++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html
@@ -45,6 +45,26 @@
+
+
+ Expiration Date
+
+
+
+
+
+
+
+
Permission
diff --git a/apps/client/src/app/components/user-account-access/user-account-access.component.ts b/apps/client/src/app/components/user-account-access/user-account-access.component.ts
index 4336cf752..0dc34c029 100644
--- a/apps/client/src/app/components/user-account-access/user-account-access.component.ts
+++ b/apps/client/src/app/components/user-account-access/user-account-access.component.ts
@@ -199,15 +199,19 @@ export class GfUserAccountAccessComponent {
}
private update() {
- this.accessesGet = this.user.access.map(({ alias, id, scopes }) => {
- return {
- id,
- scopes,
- alias: alias ?? '',
- grantee: $localize`Me`,
- type: 'PRIVATE'
- };
- });
+ this.accessesGet = this.user.access.map(
+ ({ alias, expiresAt, id, lastUsedAt, scopes }) => {
+ return {
+ expiresAt,
+ id,
+ lastUsedAt,
+ scopes,
+ alias: alias ?? '',
+ grantee: $localize`Me`,
+ type: 'PRIVATE'
+ };
+ }
+ );
this.dataService
.fetchAccesses()
diff --git a/libs/common/src/lib/dtos/create-access.dto.ts b/libs/common/src/lib/dtos/create-access.dto.ts
index f8d479230..b21621ac4 100644
--- a/libs/common/src/lib/dtos/create-access.dto.ts
+++ b/libs/common/src/lib/dtos/create-access.dto.ts
@@ -4,6 +4,7 @@ import { Scope, scopes } from '@ghostfolio/common/scopes';
import { AccessType } from '@prisma/client';
import {
IsArray,
+ IsDateString,
IsEnum,
IsIn,
IsOptional,
@@ -16,6 +17,9 @@ export class CreateAccessDto {
@IsString()
alias?: string;
+ @IsDateString()
+ expiresAt: string;
+
@IsArray()
@IsOptional()
filters?: Filter[];
diff --git a/libs/common/src/lib/dtos/update-access.dto.ts b/libs/common/src/lib/dtos/update-access.dto.ts
index c96294fc4..54e8c1596 100644
--- a/libs/common/src/lib/dtos/update-access.dto.ts
+++ b/libs/common/src/lib/dtos/update-access.dto.ts
@@ -1,13 +1,23 @@
import { Filter } from '@ghostfolio/common/interfaces';
import { Scope, scopes } from '@ghostfolio/common/scopes';
-import { IsArray, IsIn, IsOptional, IsString, IsUUID } from 'class-validator';
+import {
+ IsArray,
+ IsDateString,
+ IsIn,
+ IsOptional,
+ IsString,
+ IsUUID
+} from 'class-validator';
export class UpdateAccessDto {
@IsOptional()
@IsString()
alias?: string;
+ @IsDateString()
+ expiresAt: string;
+
@IsArray()
@IsOptional()
filters?: Filter[];
diff --git a/libs/common/src/lib/interfaces/access.interface.ts b/libs/common/src/lib/interfaces/access.interface.ts
index 1e543c86e..0cd548f98 100644
--- a/libs/common/src/lib/interfaces/access.interface.ts
+++ b/libs/common/src/lib/interfaces/access.interface.ts
@@ -4,6 +4,8 @@ import { AccessSettings } from './access-settings.interface';
export interface Access {
alias: string | null;
+ expiresAt: Date;
+ lastUsedAt?: Date | null;
grantee?: string;
id: string;
scopes: string[];
diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts
index 901c5d29d..b6493c814 100644
--- a/libs/common/src/lib/interfaces/user.interface.ts
+++ b/libs/common/src/lib/interfaces/user.interface.ts
@@ -10,7 +10,10 @@ import { UserSettings } from './user-settings.interface';
// TODO: Compare with UserWithSettings
export interface User {
- access: Pick[];
+ access: Pick<
+ Access,
+ 'alias' | 'expiresAt' | 'id' | 'lastUsedAt' | 'scopes'
+ >[];
accounts: AccountWithPlatform[];
activitiesCount: number;
activityTypes: ActivityType[];
diff --git a/prisma/migrations/20260823140000_added_expires_at_and_last_used_at_to_access/migration.sql b/prisma/migrations/20260823140000_added_expires_at_and_last_used_at_to_access/migration.sql
new file mode 100644
index 000000000..405fe4bdf
--- /dev/null
+++ b/prisma/migrations/20260823140000_added_expires_at_and_last_used_at_to_access/migration.sql
@@ -0,0 +1,12 @@
+-- AlterTable
+ALTER TABLE "Access" ADD COLUMN "expiresAt" TIMESTAMP(3),
+ADD COLUMN "lastUsedAt" TIMESTAMP(3);
+
+-- Give an access which was granted before a date in the far future, because it
+-- was granted with no expiration date
+UPDATE "Access"
+SET "expiresAt" = '2030-12-31 23:59:59.999'
+WHERE "expiresAt" IS NULL;
+
+-- AlterTable
+ALTER TABLE "Access" ALTER COLUMN "expiresAt" SET NOT NULL;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 600160124..7e4b15bfb 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -11,9 +11,11 @@ datasource db {
model Access {
alias String?
createdAt DateTime @default(now())
+ expiresAt DateTime
granteeUser User? @relation("accessGet", fields: [granteeUserId], onDelete: Cascade, references: [id])
granteeUserId String?
id String @id @default(uuid())
+ lastUsedAt DateTime?
scopes String[] @default([])
settings Json @default("{}")
type AccessType @default(PRIVATE)
|