make working apikey db

fix bug in user delete call
This commit is contained in:
rubikscraft 2022-09-02 21:28:14 +02:00
parent caa18ea3bd
commit a7981ce8ad
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
14 changed files with 170 additions and 28 deletions

View file

@ -0,0 +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';
@Module({
imports: [TypeOrmModule.forFeature([EApiKeyBackend])],
providers: [ApikeyDbService],
exports: [ApikeyDbService],
})
export class ApikeyDbModule {}

View file

@ -0,0 +1,108 @@
import { Injectable } 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';
import { generateRandomString } from 'picsur-shared/dist/util/random';
import { Repository } from 'typeorm';
import { EApiKeyBackend } from '../../database/entities/apikey.entity';
import { EUserBackend } from '../../database/entities/user.entity';
@Injectable()
export class ApikeyDbService {
constructor(
@InjectRepository(EApiKeyBackend)
private readonly apikeyRepo: Repository<EApiKeyBackend>,
) {}
async createApiKey(userid: string): AsyncFailable<EApiKeyBackend<string>> {
const apikey = new EApiKeyBackend<string>();
apikey.user = userid;
apikey.key = generateRandomString(32); // Might collide, probably not
/*
And yes it might be more secure here to sha256 the key, to ensure that they are not leaked upon db breach
But this would mean that the user has to keep track of it themselves, and it makes many other things less smooth
So just foking protect ya database, and we'll be fine
*/
try {
return this.apikeyRepo.save(apikey);
} 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');
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 },
loadRelationIds: true,
});
if (!apikey) return Fail(FT.NotFound, 'API key not found');
return apikey as EApiKeyBackend<string>;
} catch (e) {
return Fail(FT.Database, e);
}
}
async findMany(
count: number,
page: number,
userid: string | undefined,
): AsyncFailable<FindResult<EApiKeyBackend<string>>> {
if (count < 1 || page < 0) return Fail(FT.UsrValidation, 'Invalid page');
if (count > 100) return Fail(FT.UsrValidation, 'Too many results');
try {
const [apikeys, amount] = await this.apikeyRepo.findAndCount({
where: { user: userid },
skip: count * page,
take: count,
loadRelationIds: true,
});
return {
results: apikeys as EApiKeyBackend<string>[],
total: amount,
page,
pages: Math.ceil(amount / count),
};
} catch (e) {
return Fail(FT.Database, e);
}
}
async deleteApiKey(
key: string,
userid: string | undefined,
): AsyncFailable<EApiKeyBackend<string>> {
const apikeyToDelete = await this.findOne(key, userid);
if (HasFailed(apikeyToDelete)) return apikeyToDelete;
try {
return (await this.apikeyRepo.remove(
apikeyToDelete,
)) as EApiKeyBackend<string>;
} catch (e) {
return Fail(FT.Database, e);
}
}
}

View file

@ -73,15 +73,15 @@ export class UserDbService {
}
public async delete(uuid: string): AsyncFailable<EUserBackend> {
const userToModify = await this.findOne(uuid);
if (HasFailed(userToModify)) return userToModify;
const userToDelete = await this.findOne(uuid);
if (HasFailed(userToDelete)) return userToDelete;
if (UndeletableUsersList.includes(userToModify.username)) {
if (UndeletableUsersList.includes(userToDelete.username)) {
return Fail(FT.Permission, 'Cannot delete system user');
}
try {
return await this.usersRepository.remove(userToModify);
return await this.usersRepository.remove(userToDelete);
} catch (e) {
return Fail(FT.Database, e);
}

View file

@ -55,7 +55,7 @@ export class TypeOrmConfigService implements TypeOrmOptionsFactory {
const varOptions = this.getTypeOrmServerOptions();
return {
type: 'postgres' as 'postgres',
synchronize: false,
synchronize: !this.hostService.isProduction(),
migrationsRun: true,

View file

@ -1,19 +1,33 @@
import { EApiKey } from 'picsur-shared/dist/entities/apikey.entity';
import { Column, Entity, PrimaryColumn } from 'typeorm';
import { EApiKeySchema } from 'picsur-shared/dist/entities/apikey.entity';
import { CreateDateColumn, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
import { z } from 'zod';
import { EUserBackend } from './user.entity';
const OverriddenEApiKeySchema = EApiKeySchema.omit({ user: true }).merge(
z.object({
user: z.string().or(z.object({})),
}),
);
type OverriddenEApiKey = z.infer<typeof OverriddenEApiKeySchema>;
@Entity()
export class EApiKeyBackend implements EApiKey {
export class EApiKeyBackend<
T extends string | EUserBackend = string | EUserBackend,
> implements OverriddenEApiKey
{
@PrimaryColumn({
nullable: false
nullable: false,
unique: true,
})
key: string;
@Column({
@ManyToOne(() => EUserBackend, (user) => user.apikeys, {
nullable: false,
onDelete: 'CASCADE',
})
user_id: string;
user: T;
@Column({
@CreateDateColumn({
nullable: false,
})
created: Date;

View file

@ -1,6 +1,7 @@
import { EUserSchema } from 'picsur-shared/dist/entities/user.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { z } from 'zod';
import { EApiKeyBackend } from './apikey.entity';
// Different data for public and private
const OverriddenEUserSchema = EUserSchema.omit({ hashedPassword: true }).merge(
@ -24,4 +25,8 @@ export class EUserBackend implements OverriddenEUser {
@Column({ nullable: false, select: false })
hashed_password?: string;
// This will never be populated, it is only here to auto delete apikeys when a user is deleted
@OneToMany(() => EApiKeyBackend, (apikey) => apikey.user)
apikeys?: EApiKeyBackend[];
}

View file

@ -27,7 +27,7 @@ async function bootstrap() {
AppModule,
fastifyAdapter,
{
bufferLogs: true,
bufferLogs: false,
},
);

View file

@ -57,11 +57,7 @@ export class MainAuthGuard extends AuthGuard(['jwt', 'guest']) {
// These are the permissions the user has
const userPermissions = await this.usersService.getPermissions(user.id);
if (HasFailed(userPermissions)) {
throw Fail(
FT.Internal,
undefined,
'Fetching user permissions failed: ' + userPermissions.getReason(),
);
throw userPermissions
}
context.switchToHttp().getRequest().userPermissions = userPermissions;

View file

@ -1,23 +1,29 @@
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 { Fail, FT } from 'picsur-shared/dist/types';
import { NoPermissions, RequiredPermissions } from '../../../decorators/permissions.decorator';
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';
import type AuthFasityRequest from '../../../models/interfaces/authrequest.dto';
@Controller('api/experiment')
@NoPermissions()
@RequiredPermissions(Permission.Settings)
@RequiredPermissions(Permission.SysPrefAdmin)
export class ExperimentController {
constructor(
private readonly apikeyDB: ApikeyDbService,
){}
@Get()
@Returns(UserInfoResponse)
async testRoute(
@Request() req: AuthFasityRequest,
@ReqUserID() thing: string,
): Promise<UserInfoResponse> {
throw Fail(FT.NotFound, new Error("hello"));
const key = await this.apikeyDB.findOne("0SB7nCIkfhnAmf3Glejf0naUbI7dimhh", undefined);
console.log(key);
return req.user;
}
}

View file

@ -1,10 +1,12 @@
import { Module } from '@nestjs/common';
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],
controllers: [ExperimentController],
})
export class ExperimentModule {}

View file

@ -30,7 +30,7 @@ enum PicsurImgState {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PicsurImgComponent implements OnChanges {
private readonly logger = new Logger(ZodImgComponent.name);
private readonly logger = new Logger(PicsurImgComponent.name);
@ViewChild('targetcanvas') private canvas: ElementRef<HTMLCanvasElement>;
@ViewChild('targetimg') private img: ElementRef<HTMLImageElement>;

View file

@ -63,7 +63,7 @@ export class UserAdminService {
);
}
public async deleteUser(id: string): AsyncFailable<EUser> {
public async deleteUser(id: string): AsyncFailable<Omit<EUser, 'id'>> {
return await this.api.post(
UserDeleteRequest,
UserDeleteResponse,

View file

@ -33,7 +33,7 @@ export class UserCreateResponse extends createZodDto(
export const UserDeleteRequestSchema = EntityIDObjectSchema;
export class UserDeleteRequest extends createZodDto(UserDeleteRequestSchema) {}
export const UserDeleteResponseSchema = EUserSchema;
export const UserDeleteResponseSchema = EUserSchema.partial({ id: true });
export class UserDeleteResponse extends createZodDto(
UserDeleteResponseSchema,
) {}

View file

@ -3,7 +3,7 @@ import { IsEntityID } from '../validators/entity-id.validator';
export const EApiKeySchema = z.object({
key: z.string(),
user_id: IsEntityID(),
user: IsEntityID(),
created: z.preprocess((data: any) => new Date(data), z.date()),
});
export type EApiKey = z.infer<typeof EApiKeySchema>;