chore: update deps, minor bug fixes

This commit is contained in:
ryan 2023-03-31 23:40:39 +08:00
parent 5c03b8a2fe
commit fae9136e23
33 changed files with 2981 additions and 4312 deletions

View File

@ -17,6 +17,6 @@
"clean": "rm -rf ./packages/*/{tsconfig.tsbuildinfo,lib,dist,yarn-error.log,.next}"
},
"devDependencies": {
"turbo": "1.5.5"
"turbo": "1.8.8"
}
}

View File

@ -1,93 +1,94 @@
{
"name": "@ryanke/micro-api",
"version": "1.0.0",
"repository": "https://github.com/sylv/micro.git",
"author": "Ryan <ryan@sylver.me>",
"license": "GPL-3.0",
"private": true,
"type": "module",
"engine": {
"node": ">=16"
},
"scripts": {
"watch": "tsup src/main.ts src/migrations/* --target node16 --watch --clean --format esm --sourcemap --onSuccess \"node dist/main.js --inspect --inspect-brk\"",
"build": "rm -rf ./dist && ncc build src/main.ts -o dist --minify --transpile-only --v8-cache --no-source-map-register",
"lint": "eslint src --fix --cache",
"test": "vitest run"
},
"dependencies": {
"@fastify/cookie": "^8.3.0",
"@fastify/helmet": "^10.0.1",
"@fastify/multipart": "^7.2.0",
"@jenyus-org/nestjs-graphql-utils": "^1.6.4",
"@mikro-orm/core": "^5.4.2",
"@mikro-orm/migrations": "^5.4.2",
"@mikro-orm/nestjs": "^5.1.2",
"@mikro-orm/postgresql": "^5.4.2",
"@nestjs/common": "^9.1.4",
"@nestjs/core": "^9.1.4",
"@nestjs/graphql": "^10.0.16",
"@nestjs/jwt": "^9.0.0",
"@nestjs/mercurius": "^10.0.16",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-fastify": "^9.1.4",
"@nestjs/schedule": "^2.1.0",
"@ryanke/venera": "^1.0.3",
"bcryptjs": "^2.4.3",
"bytes": "^3.1.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"content-range": "^2.0.2",
"dedent": "^0.7.0",
"escape-string-regexp": "^5.0.0",
"fastify": "^4.7.0",
"file-type": "^18.0.0",
"fluent-ffmpeg": "^2.1.2",
"graphql": "^16.6.0",
"handlebars": "^4.7.7",
"istextorbinary": "^6.0.0",
"luxon": "^3.0.4",
"mercurius": "^11.0.1",
"mime-types": "^2.1.35",
"ms": "^3.0.0-canary.1",
"nanoid": "^4.0.0",
"nodemailer": "^6.8.0",
"normalize-url": "^7.2.0",
"otplib": "^12.0.1",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"pretty-bytes": "^6.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.5.7",
"sharp": "^0.31.1",
"stream-size": "^0.0.6"
},
"devDependencies": {
"@mikro-orm/cli": "^5.4.2",
"@swc/core": "^1.3.5",
"@sylo-digital/scripts": "^1.0.12",
"@types/bcryptjs": "^2.4.2",
"@types/bytes": "^3.1.1",
"@types/dedent": "^0.7.0",
"@types/fluent-ffmpeg": "^2.1.20",
"@types/luxon": "^3.0.1",
"@types/mime-types": "^2.1.1",
"@types/ms": "^0.7.31",
"@types/node": "16",
"@types/nodemailer": "^6.4.6",
"@types/passport-jwt": "^3.0.7",
"@types/sharp": "^0.31.0",
"@vercel/ncc": "^0.34.0",
"ts-node": "^10.9.1",
"tsup": "^6.2.3",
"typescript": "^4.8.4",
"vitest": "^0.24.0"
},
"mikro-orm": {
"useTsNode": true,
"configPaths": [
"./src/orm.ts",
"./dist/orm.js"
]
}
}
"name": "@ryanke/micro-api",
"version": "1.0.0",
"repository": "https://github.com/sylv/micro.git",
"author": "Ryan <ryan@sylver.me>",
"license": "GPL-3.0",
"private": true,
"type": "module",
"engine": {
"node": ">=18"
},
"scripts": {
"watch": "tsup src/main.ts src/migrations/* --target node18 --watch --clean --format esm --sourcemap --onSuccess \"node dist/main.js --inspect --inspect-brk\"",
"build": "rm -rf ./dist && ncc build src/main.ts -o dist --minify --transpile-only --v8-cache --no-source-map-register",
"lint": "eslint src --fix --cache",
"test": "vitest run",
"mikro-orm": "NODE_OPTIONS=\"--loader ts-node/esm\" mikro-orm"
},
"dependencies": {
"@fastify/cookie": "^8.3.0",
"@fastify/helmet": "^10.1.0",
"@fastify/multipart": "^7.5.0",
"@jenyus-org/graphql-utils": "^1.5.0",
"@mercuriusjs/gateway": "^1.2.0",
"@mikro-orm/core": "^5.6.15",
"@mikro-orm/migrations": "^5.6.15",
"@mikro-orm/nestjs": "^5.1.8",
"@mikro-orm/postgresql": "^5.6.15",
"@nestjs/common": "^9.3.12",
"@nestjs/core": "^9.3.12",
"@nestjs/graphql": "^11.0.4",
"@nestjs/jwt": "^10.0.3",
"@nestjs/mercurius": "^11.0.3",
"@nestjs/passport": "^9.0.3",
"@nestjs/platform-fastify": "^9.3.12",
"@nestjs/schedule": "^2.2.0",
"@ryanke/venera": "^1.0.5",
"bcryptjs": "^2.4.3",
"bytes": "^3.1.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"content-range": "^2.0.2",
"dedent": "^0.7.0",
"escape-string-regexp": "^5.0.0",
"fastify": "^4.15.0",
"file-type": "^18.2.1",
"fluent-ffmpeg": "^2.1.2",
"graphql": "^16.6.0",
"handlebars": "^4.7.7",
"istextorbinary": "^6.0.0",
"luxon": "^3.3.0",
"mercurius": "^12.2.0",
"mime-types": "^2.1.35",
"ms": "^3.0.0-canary.1",
"nanoid": "^4.0.2",
"nodemailer": "^6.9.1",
"normalize-url": "^8.0.0",
"otplib": "^12.0.1",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
"pretty-bytes": "^6.1.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0",
"sharp": "^0.32.0",
"stream-size": "^0.0.6"
},
"devDependencies": {
"@mikro-orm/cli": "^5.6.15",
"@swc/core": "^1.3.44",
"@sylo-digital/scripts": "^1.0.12",
"@types/bcryptjs": "^2.4.2",
"@types/bytes": "^3.1.1",
"@types/dedent": "^0.7.0",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/luxon": "^3.2.0",
"@types/mime-types": "^2.1.1",
"@types/ms": "^0.7.31",
"@types/node": "^18.15.11",
"@types/nodemailer": "^6.4.7",
"@types/passport-jwt": "^3.0.8",
"@types/sharp": "^0.31.1",
"@vercel/ncc": "^0.36.1",
"ts-node": "^10.9.1",
"tsup": "^6.7.0",
"typescript": "^5.0.3",
"vitest": "^0.29.8"
},
"mikro-orm": {
"useTsNode": true,
"configPaths": [
"./src/orm.config.ts"
]
}
}

View File

@ -645,12 +645,7 @@
"terms-of-service",
"terms_of_service",
"termsofservice",
"test1",
"test2",
"test3",
"teste",
"testing",
"tests",
"theme",
"themes",
"thread",

View File

@ -1,5 +1,5 @@
import { customAlphabet } from 'nanoid';
import blocklist from '../blocklist.json';
import blocklist from '../blocklist.json' assert { type: 'json' };
const contentIdLength = 6;
const paranoidIdLength = 12;

View File

@ -3,7 +3,7 @@ import { MikroORM } from '@mikro-orm/core';
import type { EntityManager } from '@mikro-orm/postgresql';
import { Logger } from '@nestjs/common';
import { checkForOldDatabase, migrateOldDatabase } from './helpers/migrate-old-database.js';
import mikroOrmConfig, { migrationsTableName, ormLogger } from './orm.js';
import mikroOrmConfig, { MIGRATIONS_TABLE_NAME, ORM_LOGGER } from './orm.config.js';
const logger = new Logger('migrate');
@ -25,13 +25,13 @@ export const migrate = async (
const executedMigrations = await migrator.getExecutedMigrations();
const pendingMigrations = await migrator.getPendingMigrations();
if (!pendingMigrations[0]) {
ormLogger.debug(`No pending migrations, ${executedMigrations.length} already executed`);
ORM_LOGGER.debug(`No pending migrations, ${executedMigrations.length} already executed`);
return;
}
ormLogger.log(`Migrating through ${pendingMigrations.length} migrations`);
ORM_LOGGER.log(`Migrating through ${pendingMigrations.length} migrations`);
await em.transactional(async (em) => {
if (!skipLock) await em.execute(`LOCK TABLE ${migrationsTableName} IN EXCLUSIVE MODE`);
if (!skipLock) await em.execute(`LOCK TABLE ${MIGRATIONS_TABLE_NAME} IN EXCLUSIVE MODE`);
await migrator.up({ transaction: em.getTransactionContext() });
});

View File

@ -50,6 +50,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"password": {
@ -210,178 +211,6 @@
}
}
},
{
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "string"
},
"user_id": {
"name": "user_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "string"
},
"expires_at": {
"name": "expires_at",
"type": "timestamptz(0)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
}
},
"name": "users_verification",
"schema": "public",
"indexes": [
{
"keyName": "users_verification_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"users_verification_user_id_foreign": {
"constraintName": "users_verification_user_id_foreign",
"columnNames": [
"user_id"
],
"localTableName": "public.users_verification",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.users",
"deleteRule": "CASCADE",
"updateRule": "cascade"
}
}
},
{
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "string"
},
"permissions": {
"name": "permissions",
"type": "int",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "integer"
},
"inviter_id": {
"name": "inviter_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "string"
},
"invited_id": {
"name": "invited_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "string"
},
"created_at": {
"name": "created_at",
"type": "timestamptz(0)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
},
"expires_at": {
"name": "expires_at",
"type": "timestamptz(0)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "invites",
"schema": "public",
"indexes": [
{
"columnNames": [
"invited_id"
],
"composite": false,
"keyName": "invites_invited_id_unique",
"primary": false,
"unique": true
},
{
"keyName": "invites_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"invites_inviter_id_foreign": {
"constraintName": "invites_inviter_id_foreign",
"columnNames": [
"inviter_id"
],
"localTableName": "public.invites",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.users",
"deleteRule": "set null",
"updateRule": "cascade"
},
"invites_invited_id_foreign": {
"constraintName": "invites_invited_id_foreign",
"columnNames": [
"invited_id"
],
"localTableName": "public.invites",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.users",
"deleteRule": "set null",
"updateRule": "cascade"
}
}
},
{
"columns": {
"id": {
@ -546,6 +375,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"created_at": {
@ -597,6 +427,127 @@
}
}
},
{
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "string"
},
"permissions": {
"name": "permissions",
"type": "int",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "integer"
},
"inviter_id": {
"name": "inviter_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "string"
},
"invited_id": {
"name": "invited_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "string"
},
"created_at": {
"name": "created_at",
"type": "timestamptz(0)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
},
"skip_verification": {
"name": "skip_verification",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"expires_at": {
"name": "expires_at",
"type": "timestamptz(0)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "invites",
"schema": "public",
"indexes": [
{
"columnNames": [
"invited_id"
],
"composite": false,
"keyName": "invites_invited_id_unique",
"primary": false,
"unique": true
},
{
"keyName": "invites_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"invites_inviter_id_foreign": {
"constraintName": "invites_inviter_id_foreign",
"columnNames": [
"inviter_id"
],
"localTableName": "public.invites",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.users",
"deleteRule": "set null",
"updateRule": "cascade"
},
"invites_invited_id_foreign": {
"constraintName": "invites_invited_id_foreign",
"columnNames": [
"invited_id"
],
"localTableName": "public.invites",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.users",
"deleteRule": "set null",
"updateRule": "cascade"
}
}
},
{
"columns": {
"id": {
@ -870,7 +821,68 @@
"id"
],
"referencedTableName": "public.files",
"deleteRule": "cascade",
"deleteRule": "CASCADE",
"updateRule": "cascade"
}
}
},
{
"columns": {
"id": {
"name": "id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "string"
},
"user_id": {
"name": "user_id",
"type": "varchar(255)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "string"
},
"expires_at": {
"name": "expires_at",
"type": "timestamptz(0)",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
}
},
"name": "users_verification",
"schema": "public",
"indexes": [
{
"keyName": "users_verification_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"users_verification_user_id_foreign": {
"constraintName": "users_verification_user_id_foreign",
"columnNames": [
"user_id"
],
"localTableName": "public.users_verification",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.users",
"deleteRule": "CASCADE",
"updateRule": "cascade"
}
}

View File

@ -0,0 +1,25 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20230331131557 extends Migration {
async up(): Promise<void> {
this.addSql('alter table "users" alter column "permissions" type int using ("permissions"::int);');
this.addSql('alter table "users" alter column "permissions" set default 0;');
this.addSql('alter table "links" alter column "clicks" type int using ("clicks"::int);');
this.addSql('alter table "links" alter column "clicks" set default 0;');
this.addSql('alter table "invites" add column "skip_verification" boolean not null default false;');
}
async down(): Promise<void> {
this.addSql('alter table "users" alter column "permissions" drop default;');
this.addSql('alter table "users" alter column "permissions" type int using ("permissions"::int);');
this.addSql('alter table "invites" drop column "skip_verification";');
this.addSql('alter table "links" alter column "clicks" drop default;');
this.addSql('alter table "links" alter column "clicks" type int using ("clicks"::int);');
}
}

View File

@ -1,4 +1,4 @@
import MikroOrmOptions from '../orm.js';
import MikroOrmOptions from '../orm.config.js';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
@ -13,8 +13,8 @@ import { HostModule } from './host/host.module.js';
import { InviteModule } from './invite/invite.module.js';
import { PasteModule } from './paste/paste.module.js';
import { StorageModule } from './storage/storage.module.js';
import { ThumbnailModule } from './thumbnail/thumbnail.module.js';
import { UserModule } from './user/user.module.js';
import { ThumbnailModule } from './thumbnail/thumbnail.module.js';
@Module({
providers: [AppResolver],
@ -26,12 +26,6 @@ import { UserModule } from './user/user.module.js';
sortSchema: true,
allowBatchedQueries: true,
autoSchemaFile: 'src/schema.gql',
errorFormatter: (execution) => {
return {
statusCode: 200,
response: execution,
};
},
}),
ScheduleModule.forRoot(),
PassportModule,

View File

@ -5,8 +5,8 @@ import { JwtService } from '@nestjs/jwt';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { authenticator } from 'otplib';
import { User } from '../user/user.entity';
import type { OTPEnabledDto } from './dto/otp-enabled.dto';
import { User } from '../user/user.entity.js';
import type { OTPEnabledDto } from './dto/otp-enabled.dto.js';
export enum TokenType {
USER = 'USER',

View File

@ -8,7 +8,7 @@ import {
OptionalProps,
PrimaryKey,
Property,
type IdentifiedReference,
type Ref,
} from '@mikro-orm/core';
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Exclude } from 'class-transformer';
@ -51,18 +51,18 @@ export class File extends Resource {
@Field({ nullable: true })
name?: string;
@OneToOne({ entity: () => Thumbnail, nullable: true, eager: true, strategy: LoadStrategy.JOINED })
@OneToOne({ entity: () => Thumbnail, nullable: true, eager: true, ref: true, strategy: LoadStrategy.JOINED })
@Field(() => Thumbnail, { nullable: true })
thumbnail?: Thumbnail;
thumbnail?: Ref<Thumbnail>;
@Property()
@Field()
createdAt: Date = new Date();
@ManyToOne(() => User, { wrappedReference: true, hidden: true })
@ManyToOne(() => User, { ref: true, hidden: true })
@Exclude()
@Index()
owner: IdentifiedReference<User>;
owner: Ref<User>;
getExtension() {
return mimeType.extension(this.type) || 'bin';

View File

@ -1,8 +1,8 @@
import { Selections } from '@jenyus-org/nestjs-graphql-utils';
import { resolveSelections } from '@jenyus-org/graphql-utils';
import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository } from '@mikro-orm/postgresql';
import { ForbiddenException, UseGuards } from '@nestjs/common';
import { Args, ID, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { Args, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import prettyBytes from 'pretty-bytes';
import { ResourceLocations } from '../../types/resource-locations.type.js';
import { UserId } from '../auth/auth.decorators.js';
@ -15,11 +15,11 @@ export class FileResolver {
@Query(() => File)
@UseGuards(OptionalJWTAuthGuard)
async file(
@Args('fileId', { type: () => ID }) fileId: string,
@Selections([{ field: 'urls', selector: 'owner' }]) populate: any[]
) {
return this.fileRepo.findOneOrFail(fileId, { populate });
async file(@Args('fileId', { type: () => ID }) fileId: string, @Info() info: any) {
const populate = resolveSelections([{ field: 'urls', selector: 'owner' }], info) as any[];
return this.fileRepo.findOneOrFail(fileId, {
populate,
});
}
@Mutation(() => Boolean)

View File

@ -1,4 +1,4 @@
import { Entity, ManyToOne, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { Entity, ManyToOne, OneToOne, OptionalProps, PrimaryKey, Property, type Ref } from '@mikro-orm/core';
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { config } from '../../config.js';
import { generateDeleteKey } from '../../helpers/generate-delete-key.helper.js';
@ -15,16 +15,20 @@ export class Invite {
@Field({ nullable: true })
permissions?: number;
@ManyToOne({ entity: () => User, nullable: true })
inviter?: User;
@ManyToOne({ entity: () => User, ref: true, nullable: true })
inviter?: Ref<User>;
@OneToOne({ entity: () => User, nullable: true })
invited?: User;
@OneToOne({ entity: () => User, ref: true, nullable: true })
invited?: Ref<User>;
@Property()
@Field()
createdAt: Date = new Date();
@Property()
@Field()
skipVerification: boolean = false;
@Property({ nullable: true })
@Field({ nullable: true })
expiresAt?: Date;
@ -53,5 +57,5 @@ export class Invite {
return `${config.rootHost.url}/${this.path}`;
}
[OptionalProps]: 'url' | 'path' | 'consumed' | 'expired' | 'createdAt';
[OptionalProps]: 'url' | 'path' | 'consumed' | 'expired' | 'createdAt' | 'skipVerification';
}

View File

@ -1,4 +1,4 @@
import { EntityRepository, MikroORM, UseRequestContext } from '@mikro-orm/core';
import { EntityRepository, MikroORM, ref, UseRequestContext } from '@mikro-orm/core';
import { InjectRepository } from '@mikro-orm/nestjs';
import type { OnApplicationBootstrap } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
@ -21,7 +21,7 @@ export class InviteService implements OnApplicationBootstrap {
protected orm: MikroORM
) {}
async create(inviterId: string | null, permissions: Permission | null) {
async create(inviterId: string | null, permissions: Permission | null, extra?: Partial<Invite>) {
const invite = this.inviteRepo.create({
inviter: inviterId || undefined,
permissions: permissions || undefined,
@ -35,9 +35,14 @@ export class InviteService implements OnApplicationBootstrap {
return this.inviteRepo.findOne(inviteId);
}
async consume(invite: Invite) {
this.inviteRepo.remove(invite);
await this.inviteRepo.flush();
async consume(invite: Invite, user: User) {
invite.invited = ref(user);
if (invite.skipVerification) {
user.verifiedEmail = true;
this.inviteRepo.persist(user);
}
await this.inviteRepo.persistAndFlush(invite);
}
@UseRequestContext()
@ -50,7 +55,7 @@ export class InviteService implements OnApplicationBootstrap {
return;
}
const invite = await this.create(null, Permission.ADMINISTRATOR);
const invite = await this.create(null, Permission.ADMINISTRATOR, { skipVerification: true });
this.logger.log(`Go to ${invite.url} to create the first account.`);
}
}

View File

@ -1,9 +1,9 @@
import { Entity, type IdentifiedReference, ManyToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { Entity, ManyToOne, OptionalProps, PrimaryKey, Property, type Ref } from '@mikro-orm/core';
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Exclude } from 'class-transformer';
import { generateContentId } from '../../helpers/generate-content-id.helper.js';
import { User } from '../user/user.entity.js';
import { Resource } from '../../helpers/resource.entity-base.js';
import { User } from '../user/user.entity.js';
@Entity({ tableName: 'links' })
@ObjectType()
@ -24,9 +24,9 @@ export class Link extends Resource {
@Field()
createdAt: Date = new Date();
@ManyToOne(() => User, { wrappedReference: true, hidden: true })
@ManyToOne(() => User, { ref: true, hidden: true })
@Exclude()
owner: IdentifiedReference<User>;
owner: Ref<User>;
getPaths() {
return {

View File

@ -1,4 +1,4 @@
import { Entity, type IdentifiedReference, ManyToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { Entity, ManyToOne, OptionalProps, PrimaryKey, Property, type Ref } from '@mikro-orm/core';
import { Field, ID, InputType, ObjectType } from '@nestjs/graphql';
import { Exclude } from 'class-transformer';
import { IsBoolean, IsNumber, IsOptional, IsString, Length } from 'class-validator';
@ -45,12 +45,8 @@ export class Paste extends Resource {
createdAt: Date = new Date();
@Exclude()
@ManyToOne(() => User, {
hidden: true,
nullable: true,
wrappedReference: true,
})
owner?: IdentifiedReference<User>;
@ManyToOne(() => User, { hidden: true, nullable: true, ref: true })
owner?: Ref<User>;
@Field({ nullable: true })
@Property({ persist: false })

View File

@ -1,8 +1,7 @@
import { HasFields, Selections } from '@jenyus-org/nestjs-graphql-utils';
import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository } from '@mikro-orm/postgresql';
import { BadRequestException, UseGuards } from '@nestjs/common';
import { Args, ID, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { Args, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { generateContentId, generateParanoidId } from '../../helpers/generate-content-id.helper.js';
import { ResourceLocations } from '../../types/resource-locations.type.js';
import { UserId } from '../auth/auth.decorators.js';
@ -11,6 +10,7 @@ import { OptionalJWTAuthGuard } from '../auth/guards/optional-jwt.guard.js';
import { HostService } from '../host/host.service.js';
import { UserService } from '../user/user.service.js';
import { CreatePasteDto, Paste } from './paste.entity.js';
import { resolveSelections, hasFields } from '@jenyus-org/graphql-utils';
@Resolver(() => Paste)
export class PasteResolver {
@ -24,15 +24,16 @@ export class PasteResolver {
@UseGuards(OptionalJWTAuthGuard)
async paste(
@UserId() userId: string,
@HasFields('paste.content') wantsContent: boolean,
@Args('pasteId', { type: () => ID }) pasteId: string,
@Selections([{ field: 'urls', selector: 'owner' }, 'content']) populate?: any[]
@Info() info: any,
@Args('pasteId', { type: () => ID }) pasteId: string
): Promise<Paste> {
const populate = resolveSelections([{ field: 'urls', selector: 'owner' }, 'content'], info) as any[];
const paste = await this.pasteRepo.findOneOrFail(pasteId, { populate });
// if the owner is viewing the paste, don't burn it
// otherwise, set that bitch alight
const isOwner = paste.owner?.id === userId;
const wantsContent = hasFields(info, 'paste.content');
if (paste.burn && !isOwner && wantsContent) {
await this.pasteRepo.removeAndFlush(paste);
paste.burnt = true;

View File

@ -3,7 +3,7 @@ import crypto from 'crypto';
import fs from 'fs';
import { nanoid } from 'nanoid';
import path from 'path';
import { ExifTransformer } from 'src/classes/ExifTransformer.js';
import { ExifTransformer } from '../../classes/ExifTransformer.js';
import { default as getSizeTransform } from 'stream-size';
import { pipeline } from 'stream/promises';
import { config } from '../../config.js';

View File

@ -1,4 +1,4 @@
import { BlobType, Entity, OneToOne, OptionalProps, PrimaryKeyType, Property } from '@mikro-orm/core';
import { BlobType, Entity, OneToOne, OptionalProps, PrimaryKeyType, Property, type Ref } from '@mikro-orm/core';
import { Field, ObjectType } from '@nestjs/graphql';
import { File } from '../file/file.entity.js';
@ -28,8 +28,8 @@ export class Thumbnail {
@Property({ type: BlobType, lazy: true, hidden: true })
data: Buffer;
@OneToOne({ entity: () => File, primary: true, onDelete: 'CASCADE' })
file: File;
@OneToOne({ entity: () => File, primary: true, ref: true, onDelete: 'CASCADE' })
file: Ref<File>;
@Property()
@Field()

View File

@ -1,6 +1,6 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsEmail, IsLowercase, IsNotIn, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
import blocklist from '../../../blocklist.json';
import blocklist from '../../../blocklist.json' assert { type: 'json' };
@InputType()
export class CreateUserDto {

View File

@ -1,9 +1,10 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsEmail } from 'class-validator';
import { IsEmail, IsOptional } from 'class-validator';
@InputType()
export class ResendVerificationEmailDto {
@IsEmail()
@IsOptional()
@Field()
email: string;
email?: string;
}

View File

@ -1,4 +1,4 @@
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core';
import { Entity, ManyToOne, PrimaryKey, Property, type Ref } from '@mikro-orm/core';
import { randomBytes } from 'crypto';
import { User } from './user.entity.js';
@ -7,8 +7,8 @@ export class UserVerification {
@PrimaryKey()
id: string = randomBytes(16).toString('hex');
@ManyToOne(() => User, { onDelete: 'CASCADE' })
user: User;
@ManyToOne(() => User, { ref: true, onDelete: 'CASCADE' })
user: Ref<User>;
@Property()
expiresAt: Date;

View File

@ -8,6 +8,7 @@ import {
OptionalProps,
PrimaryKey,
Property,
type Ref,
} from '@mikro-orm/core';
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Exclude } from 'class-transformer';
@ -48,8 +49,8 @@ export class User {
@Index()
secret: string;
@OneToOne({ nullable: true })
invite?: Invite;
@OneToOne({ entity: () => Invite, nullable: true, ref: true })
invite?: Ref<Invite>;
@Property()
@Field(() => [String])

View File

@ -114,7 +114,7 @@ export class UserService {
const verifyUrl = `${config.rootHost.url}/api/user/${verification.user.id}/verify/${verification.id}`;
const html = UserService.EMAIL_TEMPLATE({ verifyUrl });
await sendMail({
to: verification.user.email,
to: user.email,
subject: 'Verify your account | micro',
html: html,
});
@ -137,14 +137,16 @@ export class UserService {
otpEnabled: false,
});
console.log(user);
if (data.email) {
console.log('check');
await this.checkEmail(data.email);
await this.sendVerificationEmail(user);
}
try {
await this.inviteService.consume(invite);
await this.userRepo.persistAndFlush(user);
await this.inviteService.consume(invite, user);
await this.userRepo.flush();
return user;
} catch (error) {
if (error instanceof UniqueConstraintViolationException) {
@ -171,7 +173,7 @@ export class UserService {
throw new BadRequestException('Invalid or expired verification code');
}
verification.user.verifiedEmail = true;
verification.user.$.verifiedEmail = true;
await this.userRepo.persistAndFlush(verification.user);
await this.verificationRepo.nativeDelete({
user: userId,
@ -179,11 +181,13 @@ export class UserService {
}
async checkEmail(email: string) {
console.log('findOne');
const existingByLowerEmail = await this.userRepo.findOne({
email: {
$ilike: email.toLowerCase(),
},
});
console.log('findOne DONE');
if (existingByLowerEmail) {
throw new ConflictException('Username or email already exists.');

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/* eslint-disable import/no-default-export */
import { LoadStrategy } from '@mikro-orm/core';
import { FlushMode } from '@mikro-orm/core';
import type { MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs';
import { Logger, NotFoundException } from '@nestjs/common';
import { dirname, join } from 'path';
@ -15,23 +15,25 @@ import { Thumbnail } from './modules/thumbnail/thumbnail.entity.js';
import { UserVerification } from './modules/user/user-verification.entity.js';
import { User } from './modules/user/user.entity.js';
export const ormLogger = new Logger('MikroORM');
export const migrationsTableName = 'mikro_orm_migrations';
process.env.MIKRO_ORM_DYNAMIC_IMPORTS = 'true';
export const ORM_LOGGER = new Logger('MikroORM');
export const MIGRATIONS_TABLE_NAME = 'mikro_orm_migrations';
export default {
type: 'postgresql',
entities: [FileMetadata, File, Thumbnail, User, UserVerification, Invite, Paste, Link],
clientUrl: config.databaseUrl,
debug: true,
loadStrategy: LoadStrategy.JOINED,
flushMode: FlushMode.COMMIT,
logger: (message) => {
ormLogger.debug(message);
ORM_LOGGER.debug(message);
},
findOneOrFailHandler: () => {
throw new NotFoundException();
},
migrations: {
path: join(dirname(fileURLToPath(import.meta.url)), 'migrations'),
tableName: migrationsTableName,
tableName: MIGRATIONS_TABLE_NAME,
},
} as MikroOrmModuleSyncOptions;

View File

@ -87,6 +87,7 @@ type Invite {
id: ID!
path: String!
permissions: Float
skipVerification: Boolean!
url: String!
}

View File

@ -1,34 +1,31 @@
{
"include": ["src", "types"],
"compilerOptions": {
// https://www.npmjs.com/package/@tsconfig/node16
// https://www.npmjs.com/package/@tsconfig/node18
// https://github.com/sindresorhus/tsconfig/blob/main/tsconfig.json
"module": "node16",
"moduleResolution": "node16",
"moduleDetection": "force",
"target": "es2021", // node 16
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"lib": ["es2022"],
"module": "nodenext",
"target": "es2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node16",
"declaration": true,
"pretty": true,
"newLine": "lf",
"stripInternal": true,
"strict": true,
"noImplicitOverride": false,
"esModuleInterop": true,
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
"noEmitOnError": true,
"forceConsistentCasingInFileNames": true,
"importsNotUsedAsValues": "error",
"skipLibCheck": true,
"isolatedModules": true,
"outDir": "dist",
"noUncheckedIndexedAccess": false,
"strictPropertyInitialization": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2021", "dom"],
"baseUrl": ".",
"paths": {
// https://github.com/bevry/istextorbinary/issues/270

View File

@ -15,46 +15,46 @@
"generate": "graphql-codegen --config codegen.yml"
},
"dependencies": {
"@apollo/client": "^3.7.0",
"@headlessui/react": "^1.7.3",
"@apollo/client": "^3.7.10",
"@headlessui/react": "^1.7.13",
"@ryanke/pandora": "^0.0.9",
"@tailwindcss/typography": "^0.5.7",
"autoprefixer": "^10.4.12",
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",
"classnames": "^2.3.2",
"concurrently": "^7.4.0",
"copy-to-clipboard": "^3.3.2",
"dayjs": "^1.11.5",
"deepmerge": "^4.2.2",
"concurrently": "^8.0.1",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.7",
"deepmerge": "^4.3.1",
"formik": "^2.2.9",
"generate-avatar": "1.4.10",
"graphql": "^16.6.0",
"http-status-codes": "^2.2.0",
"lodash": "^4.17.21",
"nanoid": "^4.0.0",
"next": "12.2.0",
"postcss": "^8.4.17",
"nanoid": "^4.0.2",
"next": "13.2.4",
"postcss": "^8.4.21",
"prism-react-renderer": "^1.3.5",
"qrcode.react": "^3.1.0",
"react": "18.2.0",
"react-dom": "^18.1.0",
"react-feather": "^2.0.9",
"react-markdown": "^8.0.3",
"react-markdown": "^8.0.6",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"swr": "^1.3.0",
"tailwindcss": "^3.1.8",
"yup": "^0.32.11"
"swr": "^2.1.1",
"tailwindcss": "^3.3.1",
"yup": "^1.0.2"
},
"devDependencies": {
"@graphql-codegen/cli": "^2.13.5",
"@graphql-codegen/typescript": "2.7.3",
"@graphql-codegen/typescript-operations": "2.5.3",
"@graphql-codegen/typescript-react-apollo": "3.3.3",
"@graphql-codegen/cli": "^3.2.2",
"@graphql-codegen/typescript": "3.0.2",
"@graphql-codegen/typescript-operations": "3.0.2",
"@graphql-codegen/typescript-react-apollo": "3.3.7",
"@sylo-digital/scripts": "^1.0.12",
"@types/lodash": "^4.14.186",
"@types/node": "16",
"@types/react": "^18.0.21",
"prettier": "^2.7.1",
"typescript": "^4.8.4"
"@types/lodash": "^4.14.192",
"@types/node": "^18.15.11",
"@types/react": "^18.0.31",
"prettier": "^2.8.7",
"typescript": "^5.0.3"
}
}

View File

@ -1,4 +1,4 @@
import { Button, ButtonStyle, Container, useAsync, useOnClickOutside } from '@ryanke/pandora';
import { Button, ButtonStyle, Container, useAsync, useOnClickOutside, useToasts } from '@ryanke/pandora';
import classNames from 'classnames';
import { Fragment, memo, useRef, useState } from 'react';
import { Crop } from 'react-feather';
@ -17,6 +17,7 @@ export const Header = memo(() => {
const [showEmailInput, setShowEmailInput] = useState(false);
const emailInputRef = useRef<HTMLDivElement>(null);
const [email, setEmail] = useState('');
const createToast = useToasts();
const [resent, setResent] = useState(false);
const classes = classNames(
'relative z-20 flex items-center justify-between h-16 my-auto transition',
@ -30,21 +31,33 @@ export const Header = memo(() => {
const [resendMutation] = useResendVerificationEmailMutation();
const [resendVerification, sendingVerification] = useAsync(async () => {
if (!user.data) return;
if (resent || !user.data) return;
if (!user.data.email && !email) {
setShowEmailInput(true);
return;
}
const payload = !user.data.email && email ? { email } : null;
await resendMutation({
variables: {
data: payload,
},
});
try {
await resendMutation({
variables: {
data: payload,
},
});
setShowEmailInput(false);
setResent(true);
setShowEmailInput(false);
setResent(true);
} catch (error: any) {
if (error.message.includes('You can only') || error.message.includes('You have already')) {
createToast({
text: 'You have already requested a verification email. Please check your inbox, or try resend in 5 minutes.',
error: true,
});
return;
}
throw error;
}
});
return (
@ -54,8 +67,13 @@ export const Header = memo(() => {
<Container>
<span className="relative">
You must verify your email before you can upload files.{' '}
<button type="button" className="underline" onClick={resendVerification} disabled={sendingVerification}>
{resent ? 'Verification email sent' : 'Resend verification email'}
<button
type="button"
className={resent ? 'cursor-default' : 'underline'}
onClick={resendVerification}
disabled={sendingVerification || resent}
>
{resent ? `Verification email sent to ${user.data.email}!` : 'Resend verification email'}
</button>
{showEmailInput && (
<div

View File

@ -5,10 +5,6 @@ export interface LinkProps extends HTMLAttributes<HTMLAnchorElement> {
href: string;
}
export const Link: FC<LinkProps> = ({ href, children, ...rest }) => {
return (
<NextLink href={href} passHref>
<a {...rest}>{children}</a>
</NextLink>
);
export const Link: FC<LinkProps> = ({ children, ...rest }) => {
return <NextLink {...rest}>{children}</NextLink>;
};

View File

@ -25,7 +25,7 @@ export const Markdown = memo<{ children: string; className?: string }>(({ childr
return (
<div className={classes}>
<ReactMarkdown
rehypePlugins={[rehypeRaw]}
// rehypePlugins={[rehypeRaw]}
remarkPlugins={[remarkGfm]}
components={{
pre({ children }) {

View File

@ -100,6 +100,7 @@ export type Invite = {
id: Scalars['ID'];
path: Scalars['String'];
permissions?: Maybe<Scalars['Float']>;
skipVerification: Scalars['Boolean'];
url: Scalars['String'];
};

View File

@ -57,7 +57,7 @@ export default function Invite() {
return (
<Container centerY>
<Title>You&apos;re Invited</Title>
<h1 className="text-4xl font-bold text-center md:hidden">Sign Up</h1>
<h1 className="text-4xl font-bold text-center mb-6 md:hidden">Sign Up</h1>
{expiresAt && (
<p className="mt-2 mb-2 text-xs text-center text-gray-600 md:hidden">
This invite will expire <Time date={expiresAt} />.

File diff suppressed because it is too large Load Diff