allow adding names to apikeys
This commit is contained in:
parent
ec3e58d1b2
commit
5878f0ad1d
|
@ -20,6 +20,11 @@ export class ApiKeyDbService {
|
|||
const apikey = new EApiKeyBackend<string>();
|
||||
apikey.user = userid;
|
||||
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
|
||||
|
||||
/*
|
||||
|
@ -36,7 +41,7 @@ export class ApiKeyDbService {
|
|||
}
|
||||
|
||||
async findOne(
|
||||
key: string,
|
||||
id: string,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EApiKeyBackend<string>> {
|
||||
try {
|
||||
|
@ -47,7 +52,7 @@ export class ApiKeyDbService {
|
|||
? // This is stupid, but typeorm do typeorm
|
||||
({ id: userid } as any)
|
||||
: undefined,
|
||||
key,
|
||||
id,
|
||||
},
|
||||
loadRelationIds: true,
|
||||
});
|
||||
|
@ -92,11 +97,28 @@ export class ApiKeyDbService {
|
|||
}
|
||||
}
|
||||
|
||||
async deleteApiKey(
|
||||
key: string,
|
||||
async updateApiKey(
|
||||
id: string,
|
||||
name: string,
|
||||
userid: string | undefined,
|
||||
): 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;
|
||||
|
||||
const apiKeyCopy = { ...apikeyToDelete };
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { EApiKeySchema } from 'picsur-shared/dist/entities/apikey.entity';
|
||||
import {
|
||||
Column, Entity,
|
||||
ManyToOne,
|
||||
PrimaryColumn
|
||||
Index,
|
||||
ManyToOne, PrimaryGeneratedColumn
|
||||
} from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
import { EUserBackend } from './user.entity';
|
||||
|
@ -19,7 +19,11 @@ export class EApiKeyBackend<
|
|||
T extends string | EUserBackend = string | EUserBackend,
|
||||
> implements OverriddenEApiKey
|
||||
{
|
||||
@PrimaryColumn({
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
|
@ -31,6 +35,9 @@ export class EApiKeyBackend<
|
|||
})
|
||||
user: T;
|
||||
|
||||
@Column({ nullable: false })
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
|
|
|
@ -6,7 +6,9 @@ import {
|
|||
ApiKeyInfoRequest,
|
||||
ApiKeyInfoResponse,
|
||||
ApiKeyListRequest,
|
||||
ApiKeyListResponse
|
||||
ApiKeyListResponse,
|
||||
ApiKeyUpdateRequest,
|
||||
ApiKeyUpdateResponse
|
||||
} from 'picsur-shared/dist/dto/api/apikeys.dto';
|
||||
import { Permission } from 'picsur-shared/dist/dto/permissions.enum';
|
||||
import { ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
|
@ -31,7 +33,7 @@ export class ApiKeysController {
|
|||
@HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean,
|
||||
): Promise<ApiKeyInfoResponse> {
|
||||
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));
|
||||
}
|
||||
|
||||
@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')
|
||||
@Returns(ApiKeyDeleteResponse)
|
||||
async deleteApiKey(
|
||||
|
@ -65,7 +83,7 @@ export class ApiKeysController {
|
|||
@HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean,
|
||||
): Promise<ApiKeyDeleteResponse> {
|
||||
return ThrowIfFailed(
|
||||
await this.apikeyDB.deleteApiKey(body.key, isAdmin ? undefined : userID),
|
||||
await this.apikeyDB.deleteApiKey(body.id, isAdmin ? undefined : userID),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
<h1>Api Keys</h1>
|
||||
|
||||
<mat-table [dataSource]="dataSubject" class="mat-elevation-z2">
|
||||
<ng-container matColumnDef="key">
|
||||
<mat-header-cell *matHeaderCellDef>Key</mat-header-cell>
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let apikey">
|
||||
<copy-field
|
||||
label="Key"
|
||||
[value]="apikey.key"
|
||||
[hidden]="true"
|
||||
[showHideButton]="true"
|
||||
></copy-field>
|
||||
<mat-form-field class="editfield" appearance="outline">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput [value]="apikey.name" maxlength="255" (change)="updateKeyName($event, apikey.id)">
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
|
@ -34,7 +32,12 @@
|
|||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
|
||||
<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
|
||||
fontSet="material-icons-outlined"
|
||||
class="icon-red"
|
||||
|
|
|
@ -19,7 +19,7 @@ mat-table {
|
|||
color: #f44336;
|
||||
}
|
||||
|
||||
copy-field {
|
||||
.editfield {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
margin-right: 1rem;
|
||||
|
|
|
@ -21,7 +21,7 @@ export class SettingsApiKeysComponent implements OnInit {
|
|||
private readonly logger = new Logger(SettingsApiKeysComponent.name);
|
||||
|
||||
public readonly displayedColumns: string[] = [
|
||||
'key',
|
||||
'name',
|
||||
'created',
|
||||
'last_used',
|
||||
'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({
|
||||
title: `Are you sure you want to delete this api key?`,
|
||||
description: 'This action cannot be undone.',
|
||||
|
@ -100,7 +115,7 @@ export class SettingsApiKeysComponent implements OnInit {
|
|||
});
|
||||
|
||||
if (pressedButton === 'delete') {
|
||||
const result = await this.apikeysService.deleteApiKey(apikey);
|
||||
const result = await this.apikeysService.deleteApiKey(apikeyId);
|
||||
if (HasFailed(result)) {
|
||||
this.utilService.showSnackBar(
|
||||
'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()
|
||||
private subscribeToUpdate() {
|
||||
return this.updateSubject
|
||||
|
@ -156,14 +198,9 @@ export class SettingsApiKeysComponent implements OnInit {
|
|||
this.logger.warn(response.print());
|
||||
return false;
|
||||
}
|
||||
console.log(response.results);
|
||||
|
||||
if (response.results.length > 0) {
|
||||
this.dataSubject.next(response.results);
|
||||
this.totalApiKeys = response.total;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
this.dataSubject.next(response.results);
|
||||
this.totalApiKeys = response.total;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import { MatInputModule } from '@angular/material/input';
|
|||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
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 { SettingsApiKeysComponent } from './settings-apikeys.component';
|
||||
import { SettingsApiKeysRoutingModule } from './settings-apikeys.routing.module';
|
||||
|
@ -26,7 +25,6 @@ import { SettingsApiKeysRoutingModule } from './settings-apikeys.routing.module'
|
|||
MatChipsModule,
|
||||
MomentModule,
|
||||
ClipboardModule,
|
||||
CopyFieldModule,
|
||||
FabModule,
|
||||
],
|
||||
})
|
||||
|
|
|
@ -7,6 +7,7 @@ import { SidebarResolverService } from 'src/app/services/sidebar-resolver/sideba
|
|||
import { SettingsApiKeysRouteModule } from './apikeys/settings-apikeys.module';
|
||||
import { SettingsGeneralRouteModule } from './general/settings-general.module';
|
||||
import { SettingsRolesRouteModule } from './roles/settings-roles.module';
|
||||
import { SettingsShareXRouteModule } from './sharex/settings-sharex.module';
|
||||
import { SettingsSidebarComponent } from './sidebar/settings-sidebar.component';
|
||||
import { SettingsSysprefRouteModule } from './sys-pref/settings-sys-pref.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',
|
||||
loadChildren: () => SettingsUsersRouteModule,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<h1>Create a ShareX target config</h1>
|
||||
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -6,7 +6,9 @@ import {
|
|||
ApiKeyInfoRequest,
|
||||
ApiKeyInfoResponse,
|
||||
ApiKeyListRequest,
|
||||
ApiKeyListResponse
|
||||
ApiKeyListResponse,
|
||||
ApiKeyUpdateRequest,
|
||||
ApiKeyUpdateResponse,
|
||||
} from 'picsur-shared/dist/dto/api/apikeys.dto';
|
||||
import { EApiKey } from 'picsur-shared/dist/entities/apikey.entity';
|
||||
import { AsyncFailable } from 'picsur-shared/dist/types';
|
||||
|
@ -38,13 +40,13 @@ export class ApiKeysService {
|
|||
return response;
|
||||
}
|
||||
|
||||
public async getApiKey(key: string): AsyncFailable<EApiKey> {
|
||||
public async getApiKey(id: string): AsyncFailable<EApiKey> {
|
||||
return await this.api.post(
|
||||
ApiKeyInfoRequest,
|
||||
ApiKeyInfoResponse,
|
||||
'/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(
|
||||
ApiKeyDeleteRequest,
|
||||
ApiKeyDeleteResponse,
|
||||
'/api/apikeys/delete',
|
||||
{
|
||||
key,
|
||||
id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
import { EApiKeySchema } from '../../entities/apikey.entity';
|
||||
import { createZodDto } from '../../util/create-zod-dto';
|
||||
import { IsEntityID } from '../../validators/entity-id.validator';
|
||||
import { IsPosInt } from '../../validators/positive-int.validator';
|
||||
|
||||
// ApiKeyInfo
|
||||
export const ApiKeyInfoRequestSchema = EApiKeySchema.pick({
|
||||
key: true,
|
||||
export const ApiKeyInfoRequestSchema = z.object({
|
||||
id: IsEntityID(),
|
||||
});
|
||||
export class ApiKeyInfoRequest extends createZodDto(ApiKeyInfoRequestSchema) {}
|
||||
|
||||
|
@ -39,15 +40,31 @@ export class ApiKeyCreateResponse extends createZodDto(
|
|||
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
|
||||
export const ApiKeyDeleteRequestSchema = EApiKeySchema.pick({
|
||||
key: true,
|
||||
export const ApiKeyDeleteRequestSchema = z.object({
|
||||
id: IsEntityID(),
|
||||
});
|
||||
export class ApiKeyDeleteRequest extends createZodDto(
|
||||
ApiKeyDeleteRequestSchema,
|
||||
) {}
|
||||
|
||||
export const ApiKeyDeleteResponseSchema = EApiKeySchema;
|
||||
export const ApiKeyDeleteResponseSchema = EApiKeySchema.omit({
|
||||
id: true,
|
||||
});
|
||||
export class ApiKeyDeleteResponse extends createZodDto(
|
||||
ApiKeyDeleteResponseSchema,
|
||||
) {}
|
||||
|
|
|
@ -3,9 +3,11 @@ import { IsApiKey } from '../validators/api-key.validator';
|
|||
import { IsEntityID } from '../validators/entity-id.validator';
|
||||
|
||||
export const EApiKeySchema = z.object({
|
||||
id: IsEntityID(),
|
||||
key: IsApiKey(),
|
||||
user: IsEntityID(),
|
||||
name: z.string().min(3).max(255),
|
||||
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>;
|
||||
|
|
Loading…
Reference in a new issue