Skip to content

Commit

Permalink
feat: generate thumbnails ahead of time
Browse files Browse the repository at this point in the history
  • Loading branch information
sylv committed May 16, 2024
1 parent a099e3c commit 7769b86
Show file tree
Hide file tree
Showing 46 changed files with 468 additions and 451 deletions.
40 changes: 21 additions & 19 deletions packages/api/src/helpers/resource.entity-base.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { Ref } from '@mikro-orm/core';
import { BeforeCreate, Entity, Property, type EventArgs } from '@mikro-orm/core';
import { ObjectType } from '@nestjs/graphql';
import type { FastifyRequest } from 'fastify';
import { config, hosts, rootHost } from '../config.js';
import type { User } from '../modules/user/user.entity.js';
import type { ResourceLocations } from '../types/resource-locations.type.js';
import { getHostFromRequest } from './get-host-from-request.js';
import type { Ref } from "@mikro-orm/core";
import { BeforeCreate, Entity, Property, type EventArgs } from "@mikro-orm/core";
import { ObjectType } from "@nestjs/graphql";
import type { FastifyRequest } from "fastify";
import { config, hosts, rootHost } from "../config.js";
import type { UserEntity } from "../modules/user/user.entity.js";
import type { ResourceLocations } from "../types/resource-locations.type.js";
import { getHostFromRequest } from "./get-host-from-request.js";

@Entity({ abstract: true })
@ObjectType({ isAbstract: true })
export abstract class Resource {
@ObjectType("Resource", { isAbstract: true })
export abstract class ResourceEntity {
@Property({ nullable: true })
hostname?: string;

abstract owner?: Ref<User>;
abstract owner?: Ref<UserEntity>;
abstract getPaths(): ResourceLocations;

getUrls() {
Expand All @@ -31,18 +31,20 @@ export abstract class Resource {

getHost() {
if (!this.hostname) return rootHost;
const match = hosts.find((host) => host.normalised === this.hostname || host.pattern.test(this.hostname!));
const match = hosts.find(
(host) => host.normalised === this.hostname || host.pattern.test(this.hostname!),
);
if (match) return match;
return rootHost;
}

getBaseUrl() {
const owner = this.owner?.getEntity();
const host = this.getHost();
const hasPlaceholder = host.url.includes('{{username}}');
const hasPlaceholder = host.url.includes("{{username}}");
if (hasPlaceholder) {
if (!owner) return rootHost.url;
return host.url.replace('{{username}}', owner.username);
return host.url.replace("{{username}}", owner.username);
}

return host.url;
Expand All @@ -59,21 +61,21 @@ export abstract class Resource {
// root host can send all files
if (hostname === rootHost.normalised) return true;
if (this.hostname === hostname) return true;
if (this.hostname?.includes('{{username}}')) {
if (this.hostname?.includes("{{username}}")) {
// old files have {{username}} in the persisted hostname, migrating them
// to the new format is too difficult so this does a dirty comparison
// that should work for most use cases.
const withoutWildcard = this.hostname.replace('{{username}}', '');
const withoutWildcard = this.hostname.replace("{{username}}", "");
return hostname.endsWith(withoutWildcard);
}

return false;
}

@BeforeCreate()
async onBeforePersist(args: EventArgs<Resource>) {
if (args.entity.hostname?.includes('{{username}}')) {
throw new Error('Host placeholders should be replaced before insert');
async onBeforePersist(args: EventArgs<ResourceEntity>) {
if (args.entity.hostname?.includes("{{username}}")) {
throw new Error("Host placeholders should be replaced before insert");
}
}
}
32 changes: 0 additions & 32 deletions packages/api/src/migrations/.snapshot-micro.json
Original file line number Diff line number Diff line change
Expand Up @@ -694,15 +694,6 @@
"nullable": true,
"mappedType": "string"
},
"thumbnail_file_id": {
"name": "thumbnail_file_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "string"
},
"thumbnail_error": {
"name": "thumbnail_error",
"type": "varchar(255)",
Expand Down Expand Up @@ -735,16 +726,6 @@
"name": "files",
"schema": "public",
"indexes": [
{
"columnNames": [
"thumbnail_file_id"
],
"composite": false,
"keyName": "files_thumbnail_file_id_unique",
"constraint": true,
"primary": false,
"unique": true
},
{
"columnNames": [
"owner_id"
Expand All @@ -768,19 +749,6 @@
],
"checks": [],
"foreignKeys": {
"files_thumbnail_file_id_foreign": {
"constraintName": "files_thumbnail_file_id_foreign",
"columnNames": [
"thumbnail_file_id"
],
"localTableName": "public.files",
"referencedColumnNames": [
"file_id"
],
"referencedTableName": "public.thumbnails",
"deleteRule": "set null",
"updateRule": "cascade"
},
"files_owner_id_foreign": {
"constraintName": "files_owner_id_foreign",
"columnNames": [
Expand Down
18 changes: 18 additions & 0 deletions packages/api/src/migrations/Migration20240516131911.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Migration } from '@mikro-orm/migrations';

export class Migration20240516131911 extends Migration {

async up(): Promise<void> {
this.addSql('alter table "files" drop constraint "files_thumbnail_file_id_foreign";');

this.addSql('alter table "files" drop constraint "files_thumbnail_file_id_unique";');
this.addSql('alter table "files" drop column "thumbnail_file_id";');
}

async down(): Promise<void> {
this.addSql('alter table "files" add column "thumbnail_file_id" varchar(255) null;');
this.addSql('alter table "files" add constraint "files_thumbnail_file_id_foreign" foreign key ("thumbnail_file_id") references "thumbnails" ("file_id") on update cascade on delete set null;');
this.addSql('alter table "files" add constraint "files_thumbnail_file_id_unique" unique ("thumbnail_file_id");');
}

}
16 changes: 8 additions & 8 deletions packages/api/src/modules/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { config, hosts, rootHost } from '../config.js';
import { UserId } from './auth/auth.decorators.js';
import { OptionalJWTAuthGuard } from './auth/guards/optional-jwt.guard.js';
import { UserService } from './user/user.service.js';
import { type FastifyRequest } from 'fastify';
import { Controller, Get, Req, UseGuards } from "@nestjs/common";
import { config, hosts, rootHost } from "../config.js";
import { UserId } from "./auth/auth.decorators.js";
import { OptionalJWTAuthGuard } from "./auth/guards/optional-jwt.guard.js";
import { UserService } from "./user/user.service.js";
import { type FastifyRequest } from "fastify";

@Controller()
export class AppController {
constructor(private readonly userService: UserService) {}
constructor(private userService: UserService) {}

@Get('config')
@Get("config")
@UseGuards(OptionalJWTAuthGuard)
async getConfig(@Req() request: FastifyRequest, @UserId() userId?: string) {
let tags: string[] = [];
Expand Down
18 changes: 9 additions & 9 deletions packages/api/src/modules/app.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { UseGuards } from '@nestjs/common';
import { Query, Resolver } from '@nestjs/graphql';
import { config, hosts, rootHost, type MicroHost } from '../config.js';
import type { ConfigHost } from '../types/config.type.js';
import { Config } from '../types/config.type.js';
import { CurrentHost, UserId } from './auth/auth.decorators.js';
import { OptionalJWTAuthGuard } from './auth/guards/optional-jwt.guard.js';
import { UserService } from './user/user.service.js';
import { UseGuards } from "@nestjs/common";
import { Query, Resolver } from "@nestjs/graphql";
import { config, hosts, rootHost, type MicroHost } from "../config.js";
import type { ConfigHost } from "../types/config.type.js";
import { Config } from "../types/config.type.js";
import { CurrentHost, UserId } from "./auth/auth.decorators.js";
import { OptionalJWTAuthGuard } from "./auth/guards/optional-jwt.guard.js";
import { UserService } from "./user/user.service.js";

@Resolver(() => Config)
export class AppResolver {
constructor(private readonly userService: UserService) {}
constructor(private userService: UserService) {}

@Query(() => Config)
@UseGuards(OptionalJWTAuthGuard)
Expand Down
18 changes: 9 additions & 9 deletions packages/api/src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { config } from '../../config.js';
import { User } from '../user/user.entity.js';
import { AuthResolver } from './auth.resolver.js';
import { AuthService } from './auth.service.js';
import { JWTStrategy } from './strategies/jwt.strategy.js';
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { config } from "../../config.js";
import { UserEntity } from "../user/user.entity.js";
import { AuthResolver } from "./auth.resolver.js";
import { AuthService } from "./auth.service.js";
import { JWTStrategy } from "./strategies/jwt.strategy.js";

@Module({
providers: [AuthResolver, AuthService, JWTStrategy],
exports: [AuthService],
imports: [
MikroOrmModule.forFeature([User]),
MikroOrmModule.forFeature([UserEntity]),
JwtModule.register({
secret: config.secret,
}),
Expand Down
10 changes: 5 additions & 5 deletions packages/api/src/modules/auth/auth.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import { Args, Context, Mutation, Query, Resolver } from "@nestjs/graphql";
import type { FastifyReply } from "fastify";
import ms from "ms";
import { rootHost } from "../../config.js";
import { User } from "../user/user.entity.js";
import { UserEntity } from "../user/user.entity.js";
import { UserId } from "./auth.decorators.js";
import { AuthService, TokenType } from "./auth.service.js";
import { OTPEnabledDto } from "./dto/otp-enabled.dto.js";
import { JWTAuthGuard } from "./guards/jwt.guard.js";
import type { JWTPayloadUser } from "./strategies/jwt.strategy.js";

@Resolver(() => User)
@Resolver(() => UserEntity)
export class AuthResolver {
@InjectRepository(User) private readonly userRepo: EntityRepository<User>;
@InjectRepository(UserEntity) private userRepo: EntityRepository<UserEntity>;

private static readonly ONE_YEAR = ms("1y");
private static readonly COOKIE_OPTIONS = {
Expand All @@ -24,9 +24,9 @@ export class AuthResolver {
secure: rootHost.url.startsWith("https"),
};

constructor(private readonly authService: AuthService) {}
constructor(private authService: AuthService) {}

@Mutation(() => User)
@Mutation(() => UserEntity)
async login(
@Context() ctx: any,
@Args("username") username: string,
Expand Down
14 changes: 7 additions & 7 deletions packages/api/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { JwtService } from "@nestjs/jwt";
import bcrypt from "bcryptjs";
import crypto from "crypto";
import { authenticator } from "otplib";
import { User } from "../user/user.entity.js";
import { UserEntity } from "../user/user.entity.js";
import type { OTPEnabledDto } from "./dto/otp-enabled.dto.js";
import { AccountDisabledError } from "./account-disabled.error.js";
import { EntityManager } from "@mikro-orm/core";
Expand All @@ -26,10 +26,10 @@ const NUMBER_REGEX = /^\d{6}$/u;

@Injectable()
export class AuthService {
@InjectRepository(User) private readonly userRepo: EntityRepository<User>;
@InjectRepository(UserEntity) private userRepo: EntityRepository<UserEntity>;

constructor(
private readonly jwtService: JwtService,
private jwtService: JwtService,
private readonly em: EntityManager,
) {}

Expand Down Expand Up @@ -87,7 +87,7 @@ export class AuthService {
* Adds OTP codes to a user, without enabling OTP.
* This is the first step in enabling OTP, next will be to get the user to verify the code using enableOTP().
*/
async generateOTP(user: User): Promise<OTPEnabledDto> {
async generateOTP(user: UserEntity): Promise<OTPEnabledDto> {
if (user.otpEnabled) {
throw new UnauthorizedException("User already has OTP enabled.");
}
Expand Down Expand Up @@ -118,7 +118,7 @@ export class AuthService {
* Enable OTP after the user has verified the code.
* Start by calling generateOTP() to get the code.
*/
async confirmOTP(user: User, otpCode: string) {
async confirmOTP(user: UserEntity, otpCode: string) {
if (user.otpEnabled) {
throw new UnauthorizedException("User already has OTP enabled.");
}
Expand All @@ -136,7 +136,7 @@ export class AuthService {
* Disable OTP for a user.
* @param otpCode Either a recovery code or an OTP code.
*/
async disableOTP(user: User, otpCode: string) {
async disableOTP(user: UserEntity, otpCode: string) {
await this.validateOTPCode(otpCode, user);
user.otpSecret = undefined;
user.otpRecoveryCodes = undefined;
Expand All @@ -149,7 +149,7 @@ export class AuthService {
* Supports recovery codes.g
* @throws if the user does not have OTP enabled, check beforehand.
*/
private async validateOTPCode(otpCode: string | undefined, user: User) {
private async validateOTPCode(otpCode: string | undefined, user: UserEntity) {
if (!user.otpEnabled || !user.otpSecret) {
throw new Error("User does not have OTP enabled.");
}
Expand Down
25 changes: 14 additions & 11 deletions packages/api/src/modules/auth/guards/permission.guard.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import type { CanActivate, ExecutionContext } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Permission } from '../../../constants.js';
import { getRequest } from '../../../helpers/get-request.js';
import { UserService } from '../../user/user.service.js';
import { AccountDisabledError } from '../account-disabled.error.js';
import type { CanActivate, ExecutionContext } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Permission } from "../../../constants.js";
import { getRequest } from "../../../helpers/get-request.js";
import { UserService } from "../../user/user.service.js";
import { AccountDisabledError } from "../account-disabled.error.js";

@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private readonly userService: UserService, private readonly reflector: Reflector) {}
constructor(
private userService: UserService,
private readonly reflector: Reflector,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredPermissions = this.reflector.get<number | undefined>('permissions', context.getHandler());
if (requiredPermissions === undefined) throw new Error('Missing permissions definition');
const requiredPermissions = this.reflector.get<number | undefined>("permissions", context.getHandler());
if (requiredPermissions === undefined) throw new Error("Missing permissions definition");
const request = getRequest(context);
if (!request.user.id) return false;
const userId = request.user.id;
const user = await this.userService.getUser(userId, false);
if (!user) return false;
if (user.disabledReason) {
throw new AccountDisabledError(user.disabledReason)
throw new AccountDisabledError(user.disabledReason);
}

if (this.userService.checkPermissions(user.permissions, Permission.ADMINISTRATOR)) return true;
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/modules/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PassportStrategy } from "@nestjs/passport";
import type { FastifyRequest } from "fastify";
import { Strategy } from "passport-jwt";
import { config } from "../../../config.js";
import { User } from "../../user/user.entity.js";
import { UserEntity } from "../../user/user.entity.js";
import { TokenType } from "../auth.service.js";
import { AccountDisabledError } from "../account-disabled.error.js";

Expand All @@ -17,7 +17,7 @@ export interface JWTPayloadUser {

@Injectable()
export class JWTStrategy extends PassportStrategy(Strategy) {
@InjectRepository(User) private readonly userRepo: EntityRepository<User>;
@InjectRepository(UserEntity) private userRepo: EntityRepository<UserEntity>;

constructor() {
super({
Expand Down
Loading

0 comments on commit 7769b86

Please sign in to comment.