allow adding names to apikeys

This commit is contained in:
rubikscraft 2022-09-03 21:51:56 +02:00
parent ec3e58d1b2
commit 5878f0ad1d
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
16 changed files with 227 additions and 45 deletions

View file

@ -20,6 +20,11 @@ export class ApiKeyDbService {
const apikey = new EApiKeyBackend<string>(); const apikey = new EApiKeyBackend<string>();
apikey.user = userid; apikey.user = userid;
apikey.created = new Date(); apikey.created = new Date();
// YYYY-MM-DD- followed by a random number
apikey.name =
new Date().toISOString().slice(0, 10) +
'_' +
Math.round(Math.random() * 100);
apikey.key = generateRandomString(32); // Might collide, probably not apikey.key = generateRandomString(32); // Might collide, probably not
/* /*
@ -36,7 +41,7 @@ export class ApiKeyDbService {
} }
async findOne( async findOne(
key: string, id: string,
userid: string | undefined, userid: string | undefined,
): AsyncFailable<EApiKeyBackend<string>> { ): AsyncFailable<EApiKeyBackend<string>> {
try { try {
@ -47,7 +52,7 @@ export class ApiKeyDbService {
? // This is stupid, but typeorm do typeorm ? // This is stupid, but typeorm do typeorm
({ id: userid } as any) ({ id: userid } as any)
: undefined, : undefined,
key, id,
}, },
loadRelationIds: true, loadRelationIds: true,
}); });
@ -92,11 +97,28 @@ export class ApiKeyDbService {
} }
} }
async deleteApiKey( async updateApiKey(
key: string, id: string,
name: string,
userid: string | undefined, userid: string | undefined,
): AsyncFailable<EApiKeyBackend<string>> { ): AsyncFailable<EApiKeyBackend<string>> {
const apikeyToDelete = await this.findOne(key, userid); const apikey = await this.findOne(id, userid);
if (HasFailed(apikey)) return apikey;
try {
apikey.name = name;
return this.apikeyRepo.save(apikey);
} catch (e) {
return Fail(FT.Database, e);
}
}
async deleteApiKey(
id: string,
userid: string | undefined,
): AsyncFailable<EApiKeyBackend<string>> {
const apikeyToDelete = await this.findOne(id, userid);
if (HasFailed(apikeyToDelete)) return apikeyToDelete; if (HasFailed(apikeyToDelete)) return apikeyToDelete;
const apiKeyCopy = { ...apikeyToDelete }; const apiKeyCopy = { ...apikeyToDelete };

View file

@ -1,8 +1,8 @@
import { EApiKeySchema } from 'picsur-shared/dist/entities/apikey.entity'; import { EApiKeySchema } from 'picsur-shared/dist/entities/apikey.entity';
import { import {
Column, Entity, Column, Entity,
ManyToOne, Index,
PrimaryColumn ManyToOne, PrimaryGeneratedColumn
} from 'typeorm'; } from 'typeorm';
import { z } from 'zod'; import { z } from 'zod';
import { EUserBackend } from './user.entity'; import { EUserBackend } from './user.entity';
@ -19,7 +19,11 @@ export class EApiKeyBackend<
T extends string | EUserBackend = string | EUserBackend, T extends string | EUserBackend = string | EUserBackend,
> implements OverriddenEApiKey > implements OverriddenEApiKey
{ {
@PrimaryColumn({ @PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({
nullable: false, nullable: false,
unique: true, unique: true,
}) })
@ -31,6 +35,9 @@ export class EApiKeyBackend<
}) })
user: T; user: T;
@Column({ nullable: false })
name: string;
@Column({ @Column({
nullable: false, nullable: false,
}) })

View file

@ -6,7 +6,9 @@ import {
ApiKeyInfoRequest, ApiKeyInfoRequest,
ApiKeyInfoResponse, ApiKeyInfoResponse,
ApiKeyListRequest, ApiKeyListRequest,
ApiKeyListResponse ApiKeyListResponse,
ApiKeyUpdateRequest,
ApiKeyUpdateResponse
} from 'picsur-shared/dist/dto/api/apikeys.dto'; } from 'picsur-shared/dist/dto/api/apikeys.dto';
import { Permission } from 'picsur-shared/dist/dto/permissions.enum'; import { Permission } from 'picsur-shared/dist/dto/permissions.enum';
import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { ThrowIfFailed } from 'picsur-shared/dist/types';
@ -31,7 +33,7 @@ export class ApiKeysController {
@HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean, @HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean,
): Promise<ApiKeyInfoResponse> { ): Promise<ApiKeyInfoResponse> {
return ThrowIfFailed( return ThrowIfFailed(
await this.apikeyDB.findOne(body.key, isAdmin ? undefined : userid), await this.apikeyDB.findOne(body.id, isAdmin ? undefined : userid),
); );
} }
@ -57,6 +59,22 @@ export class ApiKeysController {
return ThrowIfFailed(await this.apikeyDB.createApiKey(userID)); return ThrowIfFailed(await this.apikeyDB.createApiKey(userID));
} }
@Post('update')
@Returns(ApiKeyUpdateResponse)
async updateApiKey(
@ReqUserID() userID: string,
@Body() body: ApiKeyUpdateRequest,
@HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean,
): Promise<ApiKeyUpdateResponse> {
return ThrowIfFailed(
await this.apikeyDB.updateApiKey(
body.id,
body.name,
isAdmin ? undefined : userID,
),
);
}
@Post('delete') @Post('delete')
@Returns(ApiKeyDeleteResponse) @Returns(ApiKeyDeleteResponse)
async deleteApiKey( async deleteApiKey(
@ -65,7 +83,7 @@ export class ApiKeysController {
@HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean, @HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean,
): Promise<ApiKeyDeleteResponse> { ): Promise<ApiKeyDeleteResponse> {
return ThrowIfFailed( return ThrowIfFailed(
await this.apikeyDB.deleteApiKey(body.key, isAdmin ? undefined : userID), await this.apikeyDB.deleteApiKey(body.id, isAdmin ? undefined : userID),
); );
} }
} }

View file

@ -1,15 +1,13 @@
<h1>Api Keys</h1> <h1>Api Keys</h1>
<mat-table [dataSource]="dataSubject" class="mat-elevation-z2"> <mat-table [dataSource]="dataSubject" class="mat-elevation-z2">
<ng-container matColumnDef="key"> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>Key</mat-header-cell> <mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
<mat-cell *matCellDef="let apikey"> <mat-cell *matCellDef="let apikey">
<copy-field <mat-form-field class="editfield" appearance="outline">
label="Key" <mat-label>Name</mat-label>
[value]="apikey.key" <input matInput [value]="apikey.name" maxlength="255" (change)="updateKeyName($event, apikey.id)">
[hidden]="true" </mat-form-field>
[showHideButton]="true"
></copy-field>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
@ -34,7 +32,12 @@
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell> <mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
<mat-cell *matCellDef="let apikey"> <mat-cell *matCellDef="let apikey">
<button mat-icon-button (click)="deleteApiKey(apikey.key)"> <button mat-icon-button (click)="copyKey(apikey.key)">
<mat-icon fontSet="material-icons-outlined" aria-label="Copy Key">
content_copy
</mat-icon>
</button>
<button mat-icon-button (click)="deleteApiKey(apikey.id)">
<mat-icon <mat-icon
fontSet="material-icons-outlined" fontSet="material-icons-outlined"
class="icon-red" class="icon-red"

View file

@ -19,7 +19,7 @@ mat-table {
color: #f44336; color: #f44336;
} }
copy-field { .editfield {
margin-top: 1rem; margin-top: 1rem;
width: 100%; width: 100%;
margin-right: 1rem; margin-right: 1rem;

View file

@ -21,7 +21,7 @@ export class SettingsApiKeysComponent implements OnInit {
private readonly logger = new Logger(SettingsApiKeysComponent.name); private readonly logger = new Logger(SettingsApiKeysComponent.name);
public readonly displayedColumns: string[] = [ public readonly displayedColumns: string[] = [
'key', 'name',
'created', 'created',
'last_used', 'last_used',
'actions', 'actions',
@ -82,7 +82,22 @@ export class SettingsApiKeysComponent implements OnInit {
); );
} }
public async deleteApiKey(apikey: string) { public copyKey(apikey: string) {
const result = this.clipboard.copy(apikey);
if (!result) {
this.utilService.showSnackBar(
'Failed to copy api key to clipboard',
SnackBarType.Error,
);
} else {
this.utilService.showSnackBar(
'Api key copied to clipboard',
SnackBarType.Success,
);
}
}
public async deleteApiKey(apikeyId: string) {
const pressedButton = await this.utilService.showDialog({ const pressedButton = await this.utilService.showDialog({
title: `Are you sure you want to delete this api key?`, title: `Are you sure you want to delete this api key?`,
description: 'This action cannot be undone.', description: 'This action cannot be undone.',
@ -100,7 +115,7 @@ export class SettingsApiKeysComponent implements OnInit {
}); });
if (pressedButton === 'delete') { if (pressedButton === 'delete') {
const result = await this.apikeysService.deleteApiKey(apikey); const result = await this.apikeysService.deleteApiKey(apikeyId);
if (HasFailed(result)) { if (HasFailed(result)) {
this.utilService.showSnackBar( this.utilService.showSnackBar(
'Failed to delete api key', 'Failed to delete api key',
@ -120,6 +135,33 @@ export class SettingsApiKeysComponent implements OnInit {
} }
} }
async updateKeyName(event: Event, apikeyID: string) {
const name = (event.target as HTMLInputElement).value;
if (name.length < 3) {
this.utilService.showSnackBar(
'Name must be at least 3 characters long',
SnackBarType.Warning,
);
return;
}
const result = await this.apikeysService.updateApiKey(apikeyID, name);
if (HasFailed(result)) {
this.logger.warn(result.print());
this.utilService.showSnackBar(
'Failed to update api key name',
SnackBarType.Error,
);
} else {
this.utilService.showSnackBar(
'Api key name updated',
SnackBarType.Success,
);
}
}
@AutoUnsubscribe() @AutoUnsubscribe()
private subscribeToUpdate() { private subscribeToUpdate() {
return this.updateSubject return this.updateSubject
@ -156,14 +198,9 @@ export class SettingsApiKeysComponent implements OnInit {
this.logger.warn(response.print()); this.logger.warn(response.print());
return false; return false;
} }
console.log(response.results);
if (response.results.length > 0) {
this.dataSubject.next(response.results); this.dataSubject.next(response.results);
this.totalApiKeys = response.total; this.totalApiKeys = response.total;
return true; return true;
} }
return false;
}
} }

View file

@ -8,7 +8,6 @@ import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MomentModule } from 'ngx-moment'; import { MomentModule } from 'ngx-moment';
import { CopyFieldModule } from 'src/app/components/copy-field/copy-field.module';
import { FabModule } from 'src/app/components/fab/fab.module'; import { FabModule } from 'src/app/components/fab/fab.module';
import { SettingsApiKeysComponent } from './settings-apikeys.component'; import { SettingsApiKeysComponent } from './settings-apikeys.component';
import { SettingsApiKeysRoutingModule } from './settings-apikeys.routing.module'; import { SettingsApiKeysRoutingModule } from './settings-apikeys.routing.module';
@ -26,7 +25,6 @@ import { SettingsApiKeysRoutingModule } from './settings-apikeys.routing.module'
MatChipsModule, MatChipsModule,
MomentModule, MomentModule,
ClipboardModule, ClipboardModule,
CopyFieldModule,
FabModule, FabModule,
], ],
}) })

View file

@ -7,6 +7,7 @@ import { SidebarResolverService } from 'src/app/services/sidebar-resolver/sideba
import { SettingsApiKeysRouteModule } from './apikeys/settings-apikeys.module'; import { SettingsApiKeysRouteModule } from './apikeys/settings-apikeys.module';
import { SettingsGeneralRouteModule } from './general/settings-general.module'; import { SettingsGeneralRouteModule } from './general/settings-general.module';
import { SettingsRolesRouteModule } from './roles/settings-roles.module'; import { SettingsRolesRouteModule } from './roles/settings-roles.module';
import { SettingsShareXRouteModule } from './sharex/settings-sharex.module';
import { SettingsSidebarComponent } from './sidebar/settings-sidebar.component'; import { SettingsSidebarComponent } from './sidebar/settings-sidebar.component';
import { SettingsSysprefRouteModule } from './sys-pref/settings-sys-pref.module'; import { SettingsSysprefRouteModule } from './sys-pref/settings-sys-pref.module';
import { SettingsUsersRouteModule } from './users/settings-users.module'; import { SettingsUsersRouteModule } from './users/settings-users.module';
@ -44,6 +45,18 @@ const SettingsRoutes: PRoutes = [
}, },
}, },
}, },
{
path: 'sharex',
loadChildren: () => SettingsShareXRouteModule,
data: {
permissions: [Permission.ApiKey],
page: {
title: 'ShareX',
icon: 'install_desktop',
category: 'personal',
},
},
},
{ {
path: 'users', path: 'users',
loadChildren: () => SettingsUsersRouteModule, loadChildren: () => SettingsUsersRouteModule,

View file

@ -0,0 +1,2 @@
<h1>Create a ShareX target config</h1>

View file

@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { EApiKey } from 'picsur-shared/dist/entities/apikey.entity';
import { ApiKeysService } from 'src/app/services/api/apikeys.service';
import { Logger } from 'src/app/services/logger/logger.service';
@Component({
templateUrl: './settings-sharex.component.html',
styleUrls: ['./settings-sharex.component.scss'],
})
export class SettingsShareXComponent {
private readonly logger = new Logger(SettingsShareXComponent.name);
public apikeys: EApiKey[] = [];
constructor(private readonly apikeysService: ApiKeysService) {}
private async loadApiKeys() {
}
}

View file

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SettingsShareXComponent } from './settings-sharex.component';
import { SettingsShareXRoutingModule } from './settings-sharex.routing.module';
@NgModule({
declarations: [SettingsShareXComponent],
imports: [
CommonModule,
SettingsShareXRoutingModule,
],
})
export class SettingsShareXRouteModule {}

View file

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { PRoutes } from 'src/app/models/dto/picsur-routes.dto';
import { SettingsShareXComponent } from './settings-sharex.component';
const routes: PRoutes = [
{
path: '',
component: SettingsShareXComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class SettingsShareXRoutingModule {}

View file

@ -6,7 +6,9 @@ import {
ApiKeyInfoRequest, ApiKeyInfoRequest,
ApiKeyInfoResponse, ApiKeyInfoResponse,
ApiKeyListRequest, ApiKeyListRequest,
ApiKeyListResponse ApiKeyListResponse,
ApiKeyUpdateRequest,
ApiKeyUpdateResponse,
} from 'picsur-shared/dist/dto/api/apikeys.dto'; } from 'picsur-shared/dist/dto/api/apikeys.dto';
import { EApiKey } from 'picsur-shared/dist/entities/apikey.entity'; import { EApiKey } from 'picsur-shared/dist/entities/apikey.entity';
import { AsyncFailable } from 'picsur-shared/dist/types'; import { AsyncFailable } from 'picsur-shared/dist/types';
@ -38,13 +40,13 @@ export class ApiKeysService {
return response; return response;
} }
public async getApiKey(key: string): AsyncFailable<EApiKey> { public async getApiKey(id: string): AsyncFailable<EApiKey> {
return await this.api.post( return await this.api.post(
ApiKeyInfoRequest, ApiKeyInfoRequest,
ApiKeyInfoResponse, ApiKeyInfoResponse,
'/api/apikeys/info', '/api/apikeys/info',
{ {
key, id,
}, },
); );
} }
@ -56,13 +58,25 @@ export class ApiKeysService {
); );
} }
public async deleteApiKey(key: string): AsyncFailable<EApiKey> { public async updateApiKey(id: string, name: string): AsyncFailable<EApiKey> {
return await this.api.post(
ApiKeyUpdateRequest,
ApiKeyUpdateResponse,
'/api/apikeys/update',
{
id,
name,
},
);
}
public async deleteApiKey(id: string): AsyncFailable<Omit<EApiKey, 'id'>> {
return await this.api.post( return await this.api.post(
ApiKeyDeleteRequest, ApiKeyDeleteRequest,
ApiKeyDeleteResponse, ApiKeyDeleteResponse,
'/api/apikeys/delete', '/api/apikeys/delete',
{ {
key, id,
}, },
); );
} }

View file

@ -1,11 +1,12 @@
import { z } from 'zod'; import { z } from 'zod';
import { EApiKeySchema } from '../../entities/apikey.entity'; import { EApiKeySchema } from '../../entities/apikey.entity';
import { createZodDto } from '../../util/create-zod-dto'; import { createZodDto } from '../../util/create-zod-dto';
import { IsEntityID } from '../../validators/entity-id.validator';
import { IsPosInt } from '../../validators/positive-int.validator'; import { IsPosInt } from '../../validators/positive-int.validator';
// ApiKeyInfo // ApiKeyInfo
export const ApiKeyInfoRequestSchema = EApiKeySchema.pick({ export const ApiKeyInfoRequestSchema = z.object({
key: true, id: IsEntityID(),
}); });
export class ApiKeyInfoRequest extends createZodDto(ApiKeyInfoRequestSchema) {} export class ApiKeyInfoRequest extends createZodDto(ApiKeyInfoRequestSchema) {}
@ -39,15 +40,31 @@ export class ApiKeyCreateResponse extends createZodDto(
ApiKeyCreateResponseSchema, ApiKeyCreateResponseSchema,
) {} ) {}
// ApiKeyUpdate
export const ApiKeyUpdateRequestSchema = z.object({
id: IsEntityID(),
name: z.string().max(255),
});
export class ApiKeyUpdateRequest extends createZodDto(
ApiKeyUpdateRequestSchema,
) {}
export const ApiKeyUpdateResponseSchema = EApiKeySchema;
export class ApiKeyUpdateResponse extends createZodDto(
ApiKeyUpdateResponseSchema,
) {}
// ApiKeyDelete // ApiKeyDelete
export const ApiKeyDeleteRequestSchema = EApiKeySchema.pick({ export const ApiKeyDeleteRequestSchema = z.object({
key: true, id: IsEntityID(),
}); });
export class ApiKeyDeleteRequest extends createZodDto( export class ApiKeyDeleteRequest extends createZodDto(
ApiKeyDeleteRequestSchema, ApiKeyDeleteRequestSchema,
) {} ) {}
export const ApiKeyDeleteResponseSchema = EApiKeySchema; export const ApiKeyDeleteResponseSchema = EApiKeySchema.omit({
id: true,
});
export class ApiKeyDeleteResponse extends createZodDto( export class ApiKeyDeleteResponse extends createZodDto(
ApiKeyDeleteResponseSchema, ApiKeyDeleteResponseSchema,
) {} ) {}

View file

@ -3,9 +3,11 @@ import { IsApiKey } from '../validators/api-key.validator';
import { IsEntityID } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
export const EApiKeySchema = z.object({ export const EApiKeySchema = z.object({
id: IsEntityID(),
key: IsApiKey(), key: IsApiKey(),
user: IsEntityID(), user: IsEntityID(),
name: z.string().min(3).max(255),
created: z.preprocess((data: any) => new Date(data), z.date()), created: z.preprocess((data: any) => new Date(data), z.date()),
last_used: z.preprocess((data: any) => new Date(data), z.date()).nullable() last_used: z.preprocess((data: any) => new Date(data), z.date()).nullable(),
}); });
export type EApiKey = z.infer<typeof EApiKeySchema>; export type EApiKey = z.infer<typeof EApiKeySchema>;