First commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
|
||||
|
||||
@Entity({ tableName: "config" })
|
||||
export class ConfigEntity {
|
||||
@PrimaryKey({ type: "string" })
|
||||
name!: string;
|
||||
|
||||
@Property({ type: "text" })
|
||||
value!: string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Entity, EntityRepositoryType, Index, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
|
||||
import { UserEntity } from "@/modules/entities/User";
|
||||
import { TokenRepository } from "@/modules/repositories/Token";
|
||||
|
||||
@Entity({
|
||||
tableName: "token",
|
||||
repository: () => TokenRepository,
|
||||
})
|
||||
export class TokenEntity {
|
||||
[EntityRepositoryType]?: TokenRepository;
|
||||
[OptionalProps]?: "createdAt";
|
||||
|
||||
@PrimaryKey({ type: "string", length: 64 })
|
||||
name!: string;
|
||||
|
||||
@Property({ type: "text" })
|
||||
passphrase!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity)
|
||||
@Index()
|
||||
user!: UserEntity;
|
||||
|
||||
@Property()
|
||||
isNative!: boolean;
|
||||
|
||||
@Property({ onCreate: () => new Date() })
|
||||
createdAt!: Date;
|
||||
|
||||
@Property({ nullable: true })
|
||||
lastUsedAt?: Date;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Entity, EntityRepositoryType, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
|
||||
import { UserRepository } from "@/modules/repositories/User";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
@Entity({
|
||||
tableName: "user",
|
||||
repository: () => UserRepository,
|
||||
})
|
||||
export class UserEntity {
|
||||
[EntityRepositoryType]?: UserRepository;
|
||||
[OptionalProps]?: "uuid" | "isSuspended" | "createdAt";
|
||||
|
||||
@PrimaryKey({ length: 36 })
|
||||
uuid: string = uuid();
|
||||
|
||||
@Property({ unique: true, length: 20 })
|
||||
userid!: string;
|
||||
|
||||
@Property({ length: 30 })
|
||||
username!: string;
|
||||
|
||||
@Property({ unique: true, length: 256 })
|
||||
email!: string;
|
||||
|
||||
@Property({ type: "text" })
|
||||
password!: string;
|
||||
|
||||
@Property({ default: false })
|
||||
isAdmin: boolean = false;
|
||||
|
||||
@Property({ default: false })
|
||||
isSuspended: boolean = false;
|
||||
|
||||
@Property({ onCreate: () => new Date() })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
import type { TokenEntity } from "@/modules/entities/Token";
|
||||
import { hash, argon2id, verify as argon2Verify } from "argon2";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { UserEntity } from "@/modules/entities/User";
|
||||
import { ErrorBase } from "@/errors";
|
||||
|
||||
export class TokenRepository extends EntityRepository<TokenEntity> {
|
||||
async createToken(user: UserEntity, isNative: boolean) {
|
||||
const name = randomBytes(32).toString("hex");
|
||||
const passphrase = randomBytes(32).toString("hex");
|
||||
|
||||
const hashed = await hash(passphrase, {
|
||||
type: argon2id,
|
||||
memoryCost: 2 ** 16,
|
||||
timeCost: 3,
|
||||
parallelism: 1,
|
||||
});
|
||||
|
||||
const token = this.create({
|
||||
name,
|
||||
passphrase: hashed,
|
||||
isNative,
|
||||
user,
|
||||
});
|
||||
|
||||
await this.em.persist(token).flush();
|
||||
|
||||
return `${name}_${passphrase}`;
|
||||
}
|
||||
|
||||
async authToken(
|
||||
tokenStr: string
|
||||
): Promise<TokenEntity | ReturnType<typeof ErrorBase>> {
|
||||
const tokenArr = tokenStr.split("_");
|
||||
|
||||
if (
|
||||
tokenArr[0]?.length !== 64 ||
|
||||
tokenArr[1]?.length !== 64
|
||||
)
|
||||
return ErrorBase({
|
||||
bad: "client",
|
||||
code: "token_length_wrong",
|
||||
message: "トークンの文字数が不正です。",
|
||||
});
|
||||
|
||||
const token = await this.findOne({ name: tokenArr[0] }, { populate: ["user"] });
|
||||
if (!token)
|
||||
return ErrorBase({
|
||||
bad: "client",
|
||||
code: "token_invalid",
|
||||
message: "トークンが不正です。",
|
||||
});
|
||||
|
||||
const isOk = await argon2Verify(token.passphrase, tokenArr[1]);
|
||||
if (!isOk)
|
||||
return ErrorBase({
|
||||
bad: "client",
|
||||
code: "token_invalid",
|
||||
message: "トークンが不正です。",
|
||||
});
|
||||
|
||||
token.lastUsedAt = new Date()
|
||||
await this.em.persist(token).flush();
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { EntityRepository } from "@mikro-orm/postgresql";
|
||||
import { UserEntity } from "@/modules/entities/User";
|
||||
import { hash, argon2id, verify as argon2Verify } from "argon2";
|
||||
import z from "zod/v3";
|
||||
import EmailRegex from "@/regexs/email";
|
||||
|
||||
export class UserRepository extends EntityRepository<UserEntity> {
|
||||
public static schema = z.object({
|
||||
userid: z.string().trim().min(3).max(20),
|
||||
username: z.string().trim().min(3).max(30),
|
||||
email: z.string().min(6).trim().max(254).regex(EmailRegex),
|
||||
password: z.string().trim().min(8),
|
||||
isAdmin: z.boolean(),
|
||||
});
|
||||
|
||||
async createUser(data: z.infer<typeof UserRepository.schema>) {
|
||||
const hashed = await hash(data.password, {
|
||||
type: argon2id,
|
||||
memoryCost: 2 ** 16,
|
||||
timeCost: 3,
|
||||
parallelism: 1,
|
||||
});
|
||||
|
||||
const user = this.create({
|
||||
...data,
|
||||
password: hashed,
|
||||
});
|
||||
|
||||
await this.em.persist(user).flush();
|
||||
return;
|
||||
}
|
||||
|
||||
async findByUserId(userid: string) {
|
||||
return this.findOne({ userid });
|
||||
}
|
||||
|
||||
async findByEmail(email: string) {
|
||||
return this.findOne({ email });
|
||||
}
|
||||
|
||||
async authUser(data: {
|
||||
userid: string;
|
||||
password: string;
|
||||
}) {
|
||||
const user = await this.findByUserId(data.userid);
|
||||
if (!user)
|
||||
return "userid_wrong";
|
||||
|
||||
const isOk = await argon2Verify(user.password, data.password);
|
||||
if (!isOk)
|
||||
return "password_wrong";
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user