server/database/entities/User.ts

349 lines
7 KiB
TypeScript
Raw Normal View History

2023-09-11 05:54:14 +02:00
import { getConfig, getHost } from "@config";
2023-09-13 02:29:13 +02:00
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinTable,
ManyToMany,
ManyToOne,
2023-09-22 05:18:05 +02:00
OneToMany,
2023-09-13 02:29:13 +02:00
PrimaryGeneratedColumn,
2023-09-27 01:08:05 +02:00
RemoveOptions,
2023-09-13 02:29:13 +02:00
UpdateDateColumn,
} from "typeorm";
2023-09-12 22:48:10 +02:00
import { APIAccount } from "~types/entities/account";
import { RawActor } from "./RawActor";
import { APActor } from "activitypub-types";
2023-09-20 02:16:50 +02:00
import { RawObject } from "./RawObject";
2023-09-22 03:09:14 +02:00
import { Token } from "./Token";
import { Status } from "./Status";
import { APISource } from "~types/entities/source";
2023-09-22 05:18:05 +02:00
import { Relationship } from "./Relationship";
2023-09-11 05:54:14 +02:00
const config = getConfig();
2023-09-11 05:31:08 +02:00
2023-09-12 22:48:10 +02:00
/**
* Stores local and remote users
*/
2023-09-11 05:31:08 +02:00
@Entity({
name: "users",
})
2023-09-12 22:48:10 +02:00
export class User extends BaseEntity {
2023-09-11 05:31:08 +02:00
@PrimaryGeneratedColumn("uuid")
id!: string;
2023-09-11 05:54:14 +02:00
@Column("varchar", {
unique: true,
})
2023-09-11 05:31:08 +02:00
username!: string;
2023-09-22 05:18:05 +02:00
@Column("varchar")
2023-09-13 07:30:45 +02:00
display_name!: string;
@Column("varchar")
password!: string;
2023-09-11 05:31:08 +02:00
2023-09-11 05:54:14 +02:00
@Column("varchar", {
unique: true,
})
email!: string;
@Column("varchar", {
default: "",
})
note!: string;
@Column("boolean", {
default: false,
})
is_admin!: boolean;
@Column("jsonb")
source!: APISource;
@Column("varchar")
avatar!: string;
@Column("varchar")
header!: string;
2023-09-11 05:54:14 +02:00
2023-09-11 05:31:08 +02:00
@CreateDateColumn()
created_at!: Date;
@UpdateDateColumn()
updated_at!: Date;
2023-09-11 05:54:14 +02:00
2023-09-18 07:38:08 +02:00
@Column("varchar")
public_key!: string;
@Column("varchar")
private_key!: string;
2023-09-22 05:18:05 +02:00
@OneToMany(() => Relationship, relationship => relationship.owner)
relationships!: Relationship[];
@ManyToOne(() => RawActor, actor => actor.id)
actor!: RawActor;
2023-09-20 02:16:50 +02:00
@ManyToMany(() => RawObject, object => object.id)
@JoinTable()
pinned_notes!: RawObject[];
static async getByActorId(id: string) {
return await User.createQueryBuilder("user")
// Objects is a many-to-many relationship
.leftJoinAndSelect("user.actor", "actor")
2023-09-22 05:18:05 +02:00
.leftJoinAndSelect("user.relationships", "relationships")
.where("actor.data @> :data", {
data: JSON.stringify({
id,
}),
})
.getOne();
}
static async createNew(data: {
username: string;
display_name?: string;
password: string;
email: string;
bio?: string;
avatar?: string;
header?: string;
}) {
const config = getConfig();
const user = new User();
user.username = data.username;
user.display_name = data.display_name ?? data.username;
user.password = await Bun.password.hash(data.password);
user.email = data.email;
user.note = data.bio ?? "";
user.avatar = data.avatar ?? config.defaults.avatar;
user.header = data.header ?? config.defaults.avatar;
2023-09-22 05:18:05 +02:00
user.relationships = [];
user.source = {
language: null,
note: "",
privacy: "public",
sensitive: false,
fields: [],
};
await user.generateKeys();
await user.updateActor();
await user.save();
return user;
}
2023-09-27 00:33:43 +02:00
static async retrieveFromToken(access_token: string) {
if (!access_token) return null;
const token = await Token.findOne({
where: {
access_token,
},
relations: {
user: {
relationships: true,
2023-09-27 20:45:07 +02:00
actor: true,
2023-09-27 00:33:43 +02:00
},
},
});
if (!token) return null;
return token.user;
}
2023-09-22 05:18:05 +02:00
async getRelationshipToOtherUser(other: User) {
const relationship = await Relationship.findOne({
where: {
owner: {
id: this.id,
},
subject: {
id: other.id,
},
},
relations: ["owner", "subject"],
});
return relationship;
}
2023-09-27 01:08:05 +02:00
async remove(options?: RemoveOptions | undefined) {
2023-09-22 03:09:14 +02:00
// Clean up tokens
const tokens = await Token.findBy({
user: {
id: this.id,
},
});
2023-09-27 01:08:05 +02:00
const statuses = await Status.find({
where: {
account: {
id: this.id,
},
},
relations: {
object: true,
2023-09-22 03:09:14 +02:00
},
});
// Delete both
await Promise.all(tokens.map(async token => await token.remove()));
await Promise.all(statuses.map(async status => await status.remove()));
2023-09-22 05:18:05 +02:00
// Get relationships
const relationships = await this.getRelationships();
// Delete them all
await Promise.all(
relationships.map(async relationship => await relationship.remove())
);
2023-09-27 01:08:05 +02:00
return await super.remove(options);
2023-09-22 05:18:05 +02:00
}
async getRelationships() {
const relationships = await Relationship.find({
where: {
owner: {
id: this.id,
},
},
relations: ["subject"],
});
return relationships;
2023-09-22 03:09:14 +02:00
}
async updateActor() {
// Check if actor exists
2023-09-27 20:45:07 +02:00
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2023-09-27 20:45:07 +02:00
// Check is actor already exists
const actorExists = await RawActor.getByActorId(
`${config.http.base_url}/@${this.username}`
);
let actor: RawActor;
if (actorExists) {
actor = actorExists;
} else {
actor = new RawActor();
}
actor.data = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
],
id: `${config.http.base_url}/@${this.username}`,
type: "Person",
preferredUsername: this.username,
name: this.display_name,
inbox: `${config.http.base_url}/@${this.username}/inbox`,
outbox: `${config.http.base_url}/@${this.username}/outbox`,
followers: `${config.http.base_url}/@${this.username}/followers`,
following: `${config.http.base_url}/@${this.username}/following`,
manuallyApprovesFollowers: false,
summary: this.note,
icon: {
type: "Image",
url: this.avatar,
},
image: {
type: "Image",
url: this.header,
},
publicKey: {
id: `${config.http.base_url}/@${this.username}/actor#main-key`,
owner: `${config.http.base_url}/@${this.username}/actor`,
publicKeyPem: this.public_key,
},
} as APActor;
await actor.save();
this.actor = actor;
await this.save();
return actor;
}
2023-09-18 07:38:08 +02:00
async generateKeys(): Promise<void> {
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -outform PEM -pubout -out public.pem
const keys = await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
modulusLength: 4096,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
},
true,
["sign", "verify"]
);
const privateKey = btoa(
String.fromCharCode.apply(null, [
...new Uint8Array(
await crypto.subtle.exportKey("pkcs8", keys.privateKey)
),
])
);
const publicKey = btoa(
String.fromCharCode(
...new Uint8Array(
await crypto.subtle.exportKey("spki", keys.publicKey)
)
)
);
// Add header, footer and newlines later on
// These keys are PEM encrypted
this.private_key = privateKey;
this.public_key = publicKey;
}
2023-09-12 22:48:10 +02:00
// eslint-disable-next-line @typescript-eslint/require-await
async toAPI(): Promise<APIAccount> {
2023-09-11 05:54:14 +02:00
return {
acct: `@${this.username}@${getHost()}`,
avatar: "",
avatar_static: "",
bot: false,
created_at: this.created_at.toISOString(),
2023-09-22 03:09:14 +02:00
display_name: this.display_name,
2023-09-11 05:54:14 +02:00
followers_count: 0,
following_count: 0,
group: false,
header: "",
header_static: "",
id: this.id,
locked: false,
moved: null,
noindex: false,
note: this.note,
2023-09-11 05:54:14 +02:00
suspended: false,
url: `${config.http.base_url}/@${this.username}`,
username: this.username,
emojis: [],
fields: [],
limited: false,
statuses_count: 0,
discoverable: undefined,
role: undefined,
mute_expires_at: undefined,
2023-09-13 02:29:13 +02:00
};
2023-09-11 05:54:14 +02:00
}
2023-09-13 02:29:13 +02:00
}