2026-10-03 10:36:03 +00:00
|
|
|
import type { EmailAddress, Name, UniqueID } from "@repo/rdx-ddd";
|
|
|
|
|
import { AggregateRoot } from "@repo/rdx-ddd";
|
|
|
|
|
import { Result } from "@repo/rdx-utils";
|
|
|
|
|
|
|
|
|
|
import { IdentityError } from "./identity-error";
|
2026-10-09 08:38:19 +00:00
|
|
|
import type { PasswordHash } from "./value-objects/password-hash.vo";
|
2026-10-03 10:36:03 +00:00
|
|
|
|
|
|
|
|
export type UserProps = Readonly<{
|
|
|
|
|
name: Name;
|
|
|
|
|
email: EmailAddress;
|
|
|
|
|
passwordHash: PasswordHash;
|
|
|
|
|
active: boolean;
|
|
|
|
|
isInitialAdmin: boolean;
|
|
|
|
|
authVersion: number;
|
|
|
|
|
}>;
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
export class User extends AggregateRoot<UserProps> {
|
|
|
|
|
static create(props: UserProps, id?: UniqueID): Result<User, IdentityError> {
|
|
|
|
|
if (
|
|
|
|
|
!Number.isSafeInteger(props.authVersion) ||
|
|
|
|
|
props.authVersion < 1 ||
|
|
|
|
|
props.name.toPrimitive().length > 100
|
|
|
|
|
) {
|
|
|
|
|
return Result.fail(new IdentityError("INVALID_REQUEST"));
|
|
|
|
|
}
|
|
|
|
|
if (props.isInitialAdmin && !props.active) {
|
|
|
|
|
return Result.fail(new IdentityError("ADMIN_PROTECTED"));
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
return Result.ok(new User(props, id));
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
static rehydrate(props: UserProps, id: UniqueID): User {
|
|
|
|
|
return new User(props, id);
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
get name(): Name {
|
|
|
|
|
return this.props.name;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
get email(): EmailAddress {
|
|
|
|
|
return this.props.email;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
get passwordHash(): PasswordHash {
|
|
|
|
|
return this.props.passwordHash;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
get active(): boolean {
|
|
|
|
|
return this.props.active;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
get isInitialAdmin(): boolean {
|
|
|
|
|
return this.props.isInitialAdmin;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
get authVersion(): number {
|
|
|
|
|
return this.props.authVersion;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
update(
|
|
|
|
|
values: Readonly<{ name: Name; email: EmailAddress; active: boolean }>
|
|
|
|
|
): Result<User, IdentityError> {
|
|
|
|
|
if (values.name.toPrimitive().length > 100) {
|
|
|
|
|
return Result.fail(new IdentityError("INVALID_REQUEST"));
|
|
|
|
|
}
|
|
|
|
|
if (this.isInitialAdmin && !values.active) {
|
|
|
|
|
return Result.fail(new IdentityError("ADMIN_PROTECTED"));
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
this.name.equals(values.name) &&
|
|
|
|
|
this.email.equals(values.email) &&
|
|
|
|
|
this.active === values.active
|
|
|
|
|
) {
|
|
|
|
|
return Result.ok(this);
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
return Result.ok(
|
|
|
|
|
new User(
|
|
|
|
|
{
|
|
|
|
|
...this.props,
|
|
|
|
|
...values,
|
|
|
|
|
authVersion: this.authVersion + (values.active === this.active ? 0 : 1),
|
|
|
|
|
},
|
|
|
|
|
this.id
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
changePassword(passwordHash: PasswordHash): User {
|
|
|
|
|
if (this.passwordHash.equals(passwordHash)) {
|
|
|
|
|
return this;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-03 10:36:03 +00:00
|
|
|
return new User({ ...this.props, passwordHash, authVersion: this.authVersion + 1 }, this.id);
|
|
|
|
|
}
|
|
|
|
|
}
|