add api key management endpoints
This commit is contained in:
parent
a7981ce8ad
commit
c68360c81f
|
@ -1,11 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EApiKeyBackend } from '../../database/entities/apikey.entity';
|
||||
import { ApikeyDbService } from './apikey-db.service';
|
||||
import { ApiKeyDbService } from './apikey-db.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([EApiKeyBackend])],
|
||||
providers: [ApikeyDbService],
|
||||
exports: [ApikeyDbService],
|
||||
providers: [ApiKeyDbService],
|
||||
exports: [ApiKeyDbService],
|
||||
})
|
||||
export class ApikeyDbModule {}
|
||||
export class ApiKeyDbModule {}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
|
@ -8,7 +8,9 @@ import { EApiKeyBackend } from '../../database/entities/apikey.entity';
|
|||
import { EUserBackend } from '../../database/entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ApikeyDbService {
|
||||
export class ApiKeyDbService {
|
||||
private readonly logger = new Logger(ApiKeyDbService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(EApiKeyBackend)
|
||||
private readonly apikeyRepo: Repository<EApiKeyBackend>,
|
||||
|
@ -32,28 +34,20 @@ export class ApikeyDbService {
|
|||
}
|
||||
}
|
||||
|
||||
async resolve(
|
||||
key: string
|
||||
): AsyncFailable<EApiKeyBackend<EUserBackend>> {
|
||||
try {
|
||||
const apikey = await this.apikeyRepo.findOne({
|
||||
where: { key },
|
||||
relations: ['user'],
|
||||
});
|
||||
if (!apikey) return Fail(FT.NotFound, 'API key not found');
|
||||
return apikey as EApiKeyBackend<EUserBackend>;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(
|
||||
key: string,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EApiKeyBackend<string>> {
|
||||
try {
|
||||
const apikey = await this.apikeyRepo.findOne({
|
||||
where: { user: userid, key },
|
||||
where: {
|
||||
user:
|
||||
userid !== undefined
|
||||
? // This is stupid, but typeorm do typeorm
|
||||
({ id: userid } as any)
|
||||
: undefined,
|
||||
key,
|
||||
},
|
||||
loadRelationIds: true,
|
||||
});
|
||||
if (!apikey) return Fail(FT.NotFound, 'API key not found');
|
||||
|
@ -73,7 +67,13 @@ export class ApikeyDbService {
|
|||
|
||||
try {
|
||||
const [apikeys, amount] = await this.apikeyRepo.findAndCount({
|
||||
where: { user: userid },
|
||||
where: {
|
||||
user:
|
||||
userid !== undefined
|
||||
? // This is stupid, but typeorm do typeorm
|
||||
({ id: userid } as any)
|
||||
: undefined,
|
||||
},
|
||||
skip: count * page,
|
||||
take: count,
|
||||
loadRelationIds: true,
|
||||
|
@ -97,12 +97,35 @@ export class ApikeyDbService {
|
|||
const apikeyToDelete = await this.findOne(key, userid);
|
||||
if (HasFailed(apikeyToDelete)) return apikeyToDelete;
|
||||
|
||||
const apiKeyCopy = { ...apikeyToDelete };
|
||||
try {
|
||||
return (await this.apikeyRepo.remove(
|
||||
apikeyToDelete,
|
||||
)) as EApiKeyBackend<string>;
|
||||
await this.apikeyRepo.remove(apikeyToDelete);
|
||||
return apiKeyCopy as EApiKeyBackend<string>;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
async resolve(key: string): AsyncFailable<EApiKeyBackend<EUserBackend>> {
|
||||
try {
|
||||
const apikey = await this.apikeyRepo.findOne({
|
||||
where: { key },
|
||||
relations: ['user'],
|
||||
});
|
||||
if (!apikey) return Fail(FT.NotFound, 'API key not found');
|
||||
|
||||
this.updateLastUsed(apikey);
|
||||
|
||||
return apikey as EApiKeyBackend<EUserBackend>;
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
}
|
||||
|
||||
private updateLastUsed(apikey: EApiKeyBackend) {
|
||||
(async () => {
|
||||
apikey.last_used = new Date();
|
||||
this.apikeyRepo.save(apikey);
|
||||
})().catch(this.logger.error.bind(this.logger));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { EApiKeySchema } from 'picsur-shared/dist/entities/apikey.entity';
|
||||
import { CreateDateColumn, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
PrimaryColumn
|
||||
} from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
import { EUserBackend } from './user.entity';
|
||||
|
||||
|
@ -31,4 +37,9 @@ export class EApiKeyBackend<
|
|||
nullable: false,
|
||||
})
|
||||
created: Date;
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
})
|
||||
last_used: Date;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ApiKeysModule } from './apikeys/apikeys.module';
|
||||
import { ExperimentModule } from './experiment/experiment.module';
|
||||
import { InfoModule } from './info/info.module';
|
||||
import { PrefModule } from './pref/pref.module';
|
||||
|
@ -12,6 +13,7 @@ import { UserApiModule } from './user/user.module';
|
|||
ExperimentModule,
|
||||
InfoModule,
|
||||
RolesApiModule,
|
||||
ApiKeysModule,
|
||||
],
|
||||
})
|
||||
export class PicsurApiModule {}
|
||||
|
|
71
backend/src/routes/api/apikeys/apikeys.controller.ts
Normal file
71
backend/src/routes/api/apikeys/apikeys.controller.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import {
|
||||
ApiKeyCreateResponse,
|
||||
ApiKeyDeleteRequest,
|
||||
ApiKeyDeleteResponse,
|
||||
ApiKeyInfoRequest,
|
||||
ApiKeyInfoResponse,
|
||||
ApiKeyListRequest,
|
||||
ApiKeyListResponse
|
||||
} from 'picsur-shared/dist/dto/api/apikeys.dto';
|
||||
import { Permission } from 'picsur-shared/dist/dto/permissions.enum';
|
||||
import { ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
import { ApiKeyDbService } from '../../../collections/apikey-db/apikey-db.service';
|
||||
import {
|
||||
HasPermission,
|
||||
RequiredPermissions
|
||||
} from '../../../decorators/permissions.decorator';
|
||||
import { ReqUserID } from '../../../decorators/request-user.decorator';
|
||||
import { Returns } from '../../../decorators/returns.decorator';
|
||||
|
||||
@Controller('api/apikeys')
|
||||
@RequiredPermissions(Permission.ApiKey)
|
||||
export class ApiKeysController {
|
||||
constructor(private readonly apikeyDB: ApiKeyDbService) {}
|
||||
|
||||
@Post('info')
|
||||
@Returns(ApiKeyInfoResponse)
|
||||
async getApiKeyInfo(
|
||||
@ReqUserID() userid: string,
|
||||
@Body() body: ApiKeyInfoRequest,
|
||||
@HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean,
|
||||
): Promise<ApiKeyInfoResponse> {
|
||||
return ThrowIfFailed(
|
||||
await this.apikeyDB.findOne(body.key, isAdmin ? undefined : userid),
|
||||
);
|
||||
}
|
||||
|
||||
@Post('list')
|
||||
@Returns(ApiKeyListResponse)
|
||||
async listApiKeys(
|
||||
@ReqUserID() userid: string,
|
||||
@Body() body: ApiKeyListRequest,
|
||||
@HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean,
|
||||
): Promise<ApiKeyListResponse> {
|
||||
if (!isAdmin) body.user_id = userid;
|
||||
|
||||
return ThrowIfFailed(
|
||||
await this.apikeyDB.findMany(body.count, body.page, body.user_id),
|
||||
);
|
||||
}
|
||||
|
||||
@Post('create')
|
||||
@Returns(ApiKeyCreateResponse)
|
||||
async createApiKey(
|
||||
@ReqUserID() userID: string,
|
||||
): Promise<ApiKeyCreateResponse> {
|
||||
return ThrowIfFailed(await this.apikeyDB.createApiKey(userID));
|
||||
}
|
||||
|
||||
@Post('delete')
|
||||
@Returns(ApiKeyDeleteResponse)
|
||||
async deleteApiKey(
|
||||
@ReqUserID() userID: string,
|
||||
@Body() body: ApiKeyDeleteRequest,
|
||||
@HasPermission(Permission.ApiKeyAdmin) isAdmin: boolean,
|
||||
): Promise<ApiKeyDeleteResponse> {
|
||||
return ThrowIfFailed(
|
||||
await this.apikeyDB.deleteApiKey(body.key, isAdmin ? undefined : userID),
|
||||
);
|
||||
}
|
||||
}
|
9
backend/src/routes/api/apikeys/apikeys.module.ts
Normal file
9
backend/src/routes/api/apikeys/apikeys.module.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ApiKeyDbModule } from '../../../collections/apikey-db/apikey-db.module';
|
||||
import { ApiKeysController } from './apikeys.controller';
|
||||
|
||||
@Module({
|
||||
imports: [ApiKeyDbModule],
|
||||
controllers: [ApiKeysController],
|
||||
})
|
||||
export class ApiKeysModule {}
|
|
@ -1,7 +1,7 @@
|
|||
import { Controller, Get, Request } from '@nestjs/common';
|
||||
import { UserInfoResponse } from 'picsur-shared/dist/dto/api/user-manage.dto';
|
||||
import { Permission } from 'picsur-shared/dist/dto/permissions.enum';
|
||||
import { ApikeyDbService } from '../../../collections/apikey-db/apikey-db.service';
|
||||
import { ApiKeyDbService } from '../../../collections/apikey-db/apikey-db.service';
|
||||
import { RequiredPermissions } from '../../../decorators/permissions.decorator';
|
||||
import { ReqUserID } from '../../../decorators/request-user.decorator';
|
||||
import { Returns } from '../../../decorators/returns.decorator';
|
||||
|
@ -11,7 +11,7 @@ import type AuthFasityRequest from '../../../models/interfaces/authrequest.dto';
|
|||
@RequiredPermissions(Permission.SysPrefAdmin)
|
||||
export class ExperimentController {
|
||||
constructor(
|
||||
private readonly apikeyDB: ApikeyDbService,
|
||||
private readonly apikeyDB: ApiKeyDbService,
|
||||
){}
|
||||
|
||||
@Get()
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ApikeyDbModule } from '../../../collections/apikey-db/apikey-db.module';
|
||||
import { ApiKeyDbModule } from '../../../collections/apikey-db/apikey-db.module';
|
||||
import { ExperimentController } from './experiment.controller';
|
||||
|
||||
// This is comletely useless module, but is used for testing
|
||||
// TODO: remove when out of beta
|
||||
|
||||
@Module({
|
||||
imports: [ApikeyDbModule],
|
||||
imports: [ApiKeyDbModule],
|
||||
controllers: [ExperimentController],
|
||||
})
|
||||
export class ExperimentModule {}
|
||||
|
|
|
@ -12,8 +12,11 @@ export const UIFriendlyPermissions: {
|
|||
|
||||
[Permission.Settings]: 'View settings',
|
||||
|
||||
[Permission.ApiKey]: 'Use API keys',
|
||||
|
||||
[Permission.ImageAdmin]: 'Image Admin',
|
||||
[Permission.UserAdmin]: 'User Admin',
|
||||
[Permission.RoleAdmin]: 'Role Admin',
|
||||
[Permission.ApiKeyAdmin]: 'API Key Admin',
|
||||
[Permission.SysPrefAdmin]: 'System Admin',
|
||||
};
|
||||
|
|
53
shared/src/dto/api/apikeys.dto.ts
Normal file
53
shared/src/dto/api/apikeys.dto.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { z } from 'zod';
|
||||
import { EApiKeySchema } from '../../entities/apikey.entity';
|
||||
import { createZodDto } from '../../util/create-zod-dto';
|
||||
import { IsPosInt } from '../../validators/positive-int.validator';
|
||||
|
||||
// ApiKeyInfo
|
||||
export const ApiKeyInfoRequestSchema = EApiKeySchema.pick({
|
||||
key: true,
|
||||
});
|
||||
export class ApiKeyInfoRequest extends createZodDto(ApiKeyInfoRequestSchema) {}
|
||||
|
||||
export const ApiKeyInfoResponseSchema = EApiKeySchema;
|
||||
export class ApiKeyInfoResponse extends createZodDto(
|
||||
ApiKeyInfoResponseSchema,
|
||||
) {}
|
||||
|
||||
// ApiKeyList
|
||||
|
||||
export const ApiKeyListRequestSchema = z.object({
|
||||
count: IsPosInt(),
|
||||
page: IsPosInt(),
|
||||
user_id: z.string().uuid().optional(),
|
||||
});
|
||||
export class ApiKeyListRequest extends createZodDto(ApiKeyListRequestSchema) {}
|
||||
|
||||
export const ApiKeyListResponseSchema = z.object({
|
||||
results: z.array(EApiKeySchema),
|
||||
total: IsPosInt(),
|
||||
page: IsPosInt(),
|
||||
pages: IsPosInt(),
|
||||
});
|
||||
export class ApiKeyListResponse extends createZodDto(
|
||||
ApiKeyListResponseSchema,
|
||||
) {}
|
||||
|
||||
// ApiKeyCreate
|
||||
export const ApiKeyCreateResponseSchema = EApiKeySchema;
|
||||
export class ApiKeyCreateResponse extends createZodDto(
|
||||
ApiKeyCreateResponseSchema,
|
||||
) {}
|
||||
|
||||
// ApiKeyDelete
|
||||
export const ApiKeyDeleteRequestSchema = EApiKeySchema.pick({
|
||||
key: true,
|
||||
});
|
||||
export class ApiKeyDeleteRequest extends createZodDto(
|
||||
ApiKeyDeleteRequestSchema,
|
||||
) {}
|
||||
|
||||
export const ApiKeyDeleteResponseSchema = EApiKeySchema;
|
||||
export class ApiKeyDeleteResponse extends createZodDto(
|
||||
ApiKeyDeleteResponseSchema,
|
||||
) {}
|
|
@ -12,8 +12,11 @@ export enum Permission {
|
|||
|
||||
Settings = 'settings', // Ability to view (personal) settings
|
||||
|
||||
ApiKey = 'apikey', // Ability to create and remove your own api keys
|
||||
|
||||
ImageAdmin = 'image-admin', // Ability to manage everyones manage images
|
||||
UserAdmin = 'user-admin', // Allow modification of users
|
||||
RoleAdmin = 'role-admin', // Allow modification of roles
|
||||
ApiKeyAdmin = 'apikey-admin', // Allow modification of all api keys
|
||||
SysPrefAdmin = 'syspref-admin',
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { z } from 'zod';
|
||||
import { IsApiKey } from '../validators/api-key.validator';
|
||||
import { IsEntityID } from '../validators/entity-id.validator';
|
||||
|
||||
export const EApiKeySchema = z.object({
|
||||
key: z.string(),
|
||||
key: IsApiKey(),
|
||||
user: IsEntityID(),
|
||||
created: z.preprocess((data: any) => new Date(data), z.date()),
|
||||
created: z.date(),
|
||||
last_used: z.date().nullable(),
|
||||
});
|
||||
export type EApiKey = z.infer<typeof EApiKeySchema>;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { IsEntityID } from '../validators/entity-id.validator';
|
|||
export const EImageSchema = z.object({
|
||||
id: IsEntityID(),
|
||||
user_id: IsEntityID(),
|
||||
created: z.preprocess((data: any) => new Date(data), z.date()),
|
||||
created: z.date(),
|
||||
file_name: z.string(),
|
||||
});
|
||||
export type EImage = z.infer<typeof EImageSchema>;
|
||||
|
|
4
shared/src/validators/api-key.validator.ts
Normal file
4
shared/src/validators/api-key.validator.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const IsApiKey = () =>
|
||||
z.string().regex(/^[a-zA-Z0-9]{32}$/, 'Invalid API key');
|
Loading…
Reference in a new issue