Spaces:
Sleeping
Sleeping
File size: 4,897 Bytes
a0eafe5 9a951d7 8a24f2c ec9da6a 8a24f2c aa95439 b4d141a ec9da6a 9ca537f 90c9777 9a951d7 90c9777 86063f3 a0eafe5 fe607c0 9a951d7 90c9777 9a951d7 8a24f2c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | import { Entity, PrimaryGeneratedColumn, Column, Index, OneToOne, OneToMany } from 'typeorm';
import { UserProfile } from './entities/user-profile.entity';
import { UserGoal } from './entities/user-goal.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ unique: true })
email!: string;
@Column({ type: 'varchar', nullable: true })
password: string | null;
@Column({ type:'varchar', nullable: true, unique: true }) // <-- Google ID field
googleId: string | null;
@Column()
name!: string;
@Column({ default: false })
isVerified!: boolean;
@Column({ type: 'text', nullable: true })
refreshTokenHash: string | null;
@Column({ type: 'varchar' , nullable: true })
otpHash: string | null; // store hashed OTP
@Column({ type: 'timestamptz', nullable: true })
otpExpiry: Date | null;
@Column({ type: 'timestamp', nullable: true })
refreshTokenIssuedAt: Date | null;
@Column({ type: 'text', nullable: true })
resetTokenHash: string | null;
@Column({ type: 'timestamp', nullable: true })
resetTokenExpiry: Date | null;
@Column({ default: false })
isLoggedin!: boolean;
/**
* Access role. Added because the condition AdminKeyGuard's docblock set β
* "if an admin dashboard is built later, replace this with a proper role
* guard" β has now been met by the campaign admin UI.
*
* There is deliberately no self-serve path to 'admin': the column is set by
* hand in the database. AdminKeyGuard stays in place on the existing scraper
* maintenance routes; migrating those is unrelated risk.
*/
@Column({ type: 'varchar', length: 16, default: 'candidate' })
role!: 'candidate' | 'admin';
/**
* Which campaign brought this user in β null for organic signups.
*
* This is the question the whole apply flow exists to answer: did posting a
* role on LinkedIn actually acquire users, or did it just fill one seat? A
* campaign that fills its role and adds nobody to the platform is a different
* outcome from one that does both, and only this column can tell them apart.
*/
@Column({ type: 'uuid', nullable: true })
sourceCampaignId!: string | null;
/**
* Whether this person is currently open to being put forward for roles.
*
* `suspended` is set automatically when someone is submitted to companies
* repeatedly and never turns up to the interview - see
* CampaignsService.reviewAvailability. It is deliberately NOT a rejection or
* a ban: it stops us spending a client's goodwill on a candidate who is not
* actually available, and it lifts the moment they say otherwise.
*
* Only the candidate can lift it, by signing in and setting it back. An
* operator cannot flip it for them: the whole point is to find out whether
* the person is still looking, and only they can answer that.
*/
@Column({ type: 'varchar', length: 24, default: 'looking' })
availability!: 'looking' | 'suspended';
@Column({ type: 'timestamptz', nullable: true })
availabilityUpdatedAt!: Date | null;
/** Shown to the candidate, so a suspension is never unexplained. */
@Column({ type: 'text', nullable: true })
suspensionReason!: string | null;
/**
* Last time the user did something deliberate β tracked an application,
* updated one, or ran a custom search. Deliberately NOT bumped by feed reads:
* a page load is not intent, and counting it would make everyone look active
* forever.
*
* Stored as a timestamp rather than an `isActive` flag on purpose. Nothing has
* to write "inactive" β no request arrives when a user stops caring β so a flag
* would need a nightly sweep to stay honest and would drift whenever that
* sweep failed. A timestamp goes stale on its own, and the activity window
* becomes a number you can change without a migration or a backfill.
*/
@Index()
@Column({ type: 'timestamptz', nullable: true })
lastActiveAt: Date | null;
/**
* When this account was created.
*
* Added late, which is why it is nullable rather than a @CreateDateColumn.
* That decorator would have stamped every existing row with the deploy time
* under `synchronize: true`, so the whole user base would have appeared to
* sign up on the same afternoon - a wrong answer presented with total
* confidence. NULL says "before we started recording this", which is true,
* and the admin list sorts those last and labels them.
*
* scripts/backfill-user-created-at.mjs recovers a real date for most of them
* from the earliest thing they did.
*/
@Index()
@Column({ type: 'timestamptz', nullable: true })
createdAt: Date | null;
// Relations
@OneToOne(() => UserProfile, profile => profile.user, { cascade: true })
profile: UserProfile;
@OneToMany(() => UserGoal, goal => goal.user, { cascade: true })
goals: UserGoal[];
}
|