SmartfolioBackend / src /modules /users /user.entity.ts
ibrahimgulbutt's picture
deploy: users admin tab, signup dates, campaign vs direct source
fe607c0
Raw History Blame Contribute Delete
4.9 kB
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[];
}