Rework user types: create AuthenticatedUser and AnonymousUser class

Both are subclasses of an abstract User class which contains almost everything
interesting.
This commit is contained in:
2026-01-17 17:45:36 -06:00
parent 350bf7c865
commit d921679058
9 changed files with 102 additions and 62 deletions

View File

@@ -51,39 +51,15 @@ const defaultRolePermissions: RolePermissionMap = new Map([
["user", ["users:read"]],
]);
export class User {
private readonly data: UserData;
private rolePermissions: RolePermissionMap;
export abstract class User {
protected readonly data: UserData;
protected rolePermissions: RolePermissionMap;
constructor(data: UserData, rolePermissions?: RolePermissionMap) {
this.data = userDataParser.parse(data);
this.rolePermissions = rolePermissions ?? defaultRolePermissions;
}
// Factory for creating new users with sensible defaults
static create(
email: string,
options?: {
id?: string;
displayName?: string;
status?: UserStatus;
roles?: Role[];
permissions?: Permission[];
},
): User {
const now = new Date();
return new User({
id: options?.id ?? crypto.randomUUID(),
email,
displayName: options?.displayName,
status: options?.status ?? "active",
roles: options?.roles ?? [],
permissions: options?.permissions ?? [],
createdAt: now,
updatedAt: now,
});
}
// Identity
get id(): UserId {
return this.data.id as UserId;
@@ -185,15 +161,72 @@ export class User {
toString(): string {
return `User(id ${this.id})`;
}
abstract isAnonymous(): boolean;
}
export class AuthenticatedUser extends User {
// Factory for creating new users with sensible defaults
static create(
email: string,
options?: {
id?: string;
displayName?: string;
status?: UserStatus;
roles?: Role[];
permissions?: Permission[];
},
): User {
const now = new Date();
return new AuthenticatedUser({
id: options?.id ?? crypto.randomUUID(),
email,
displayName: options?.displayName,
status: options?.status ?? "active",
roles: options?.roles ?? [],
permissions: options?.permissions ?? [],
createdAt: now,
updatedAt: now,
});
}
isAnonymous(): boolean {
return false;
}
}
// For representing "no user" in contexts where user is optional
export const AnonymousUser = Symbol("AnonymousUser");
export class AnonymousUser extends User {
// FIXME: this is C&Ped with only minimal changes. No bueno.
static create(
email: string,
options?: {
id?: string;
displayName?: string;
status?: UserStatus;
roles?: Role[];
permissions?: Permission[];
},
): AnonymousUser {
const now = new Date(0);
return new AnonymousUser({
id: options?.id ?? crypto.randomUUID(),
email,
displayName: options?.displayName,
status: options?.status ?? "active",
roles: options?.roles ?? [],
permissions: options?.permissions ?? [],
createdAt: now,
updatedAt: now,
});
}
export const anonymousUser = User.create("anonymous@example.com", {
isAnonymous(): boolean {
return true;
}
}
export const anonymousUser = AnonymousUser.create("anonymous@example.com", {
id: "-1",
displayName: "Anonymous User",
// FIXME: set createdAt and updatedAt to start of epoch
});
export type MaybeUser = User | typeof AnonymousUser;