User API
GraphQL API reference for user management -- authentication, profiles, onboarding, company membership, and account deletion.
Authentication
Login & Registration
| Operation | Type | Auth | Description |
|---|---|---|---|
signIn | Mutation | Public | Email/password login, returns AuthPayload (token, temporaryToken, message, user) |
signUp | Mutation | Public | Create account (SignUpsInput: email, password, confirmPassword, firstName, lastName) |
verifyEmail | Mutation | Public | Verify email via token or OTP |
Password Management
| Operation | Type | Auth | Description |
|---|---|---|---|
requestPasswordReset | Mutation | Public | Send password reset email (RequestPasswordResetInput). Reset token TTL is 24 hours (SUP-00006, 2026-04-21 — bumped from 1h because users reported links expiring before they opened the email). See EXPIRY_TIMES.PASSWORD_RESET in src/common/constants/time.constants.ts. |
resetPassword | Mutation | Public | Reset password with token (24h valid, single-use, deleted after redemption) |
Two-Factor Authentication
| Operation | Type | Auth | Description |
|---|---|---|---|
enableTwoFactor | Mutation | JWT | Enable/disable 2FA |
verifyTwoFactor | Mutation | Public | Verify 2FA code during login |
JWT tokens are issued on login and must be included in the Authorization: Bearer <token> header for all protected operations. Tokens are validated by JwtAuthGuard.
User Profile Queries
whoAmI -- Current User
Returns the authenticated user's full profile with company roles and subscription.
query WhoAmI {
whoAmI {
id
email
firstName
lastName
title
location
biography
profileImageUrl
viewMode
selectedCompanyId
isVerified
isSuperAdmin
twoFactorEnabled
UserCompanyRole {
Role { name }
Company { companyName }
}
}
}
Auth: JWT required. Uses @CurrentUser() decorator to inject the authenticated user.
userByEmail -- Lookup by Email
query GetUserByEmail($email: String!) {
userByEmail(email: $email) {
id
email
firstName
lastName
isOnboarding
isVerified
isActive
UserCompanyRole {
Role { name }
Company { companyName }
}
}
}
Auth: JWT required. Email is normalized to lowercase before lookup.
user -- Lookup by ID
query GetUser($id: ID!) {
user(id: $id) {
id
email
firstName
lastName
title
location
}
}
User Onboarding
createOnboardedUser
Creates a new user with optional company talent pool assignment and CV data. This is the primary endpoint for adding users to the platform programmatically.
mutation CreateOnboardedUser($input: CreateOnboardedUserInput!) {
createOnboardedUser(input: $input) {
id
email
firstName
lastName
isOnboarding
isVerified
UserCompanyRole {
Role { name }
Company { companyName }
}
}
}
Auth: Public endpoint (@SetMetadata('isPublic', true)).
Key input fields:
| Field | Type | Required | Description |
|---|---|---|---|
email | String | Yes | Must be valid email |
password | String | Yes | Min 8 characters |
firstName | String | No | Min 2 characters |
lastName | String | No | Min 2 characters |
viewMode | ViewMode | No | EMPLOYER or JOB_SEEKER |
talentPoolId | String | No | Assign to company talent pool |
cvText | String | No | CV content for parsing |
sendInvitation | Boolean | No | Send verification email |
desiredJobTitles | [String] | No | Job seeker preferences |
preferredJobTypes | [PreferredJobType] | No | FULL_TIME, PART_TIME, etc. |
expectedSalary | Float | No | Expected compensation |
Behavior:
- If user already exists, adds them to the specified talent pool
- New users are created with
isVerified: false,isActive: false - When
talentPoolIdis provided, creates a Talent record in the company's default pool - CV data is structured into 11 CV tables +
CVSkillrecords - Email verification is non-blocking (failure does not rollback user creation)
User Updates
updateUser
Update profile fields for a user.
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
firstName
lastName
title
location
biography
viewMode
selectedCompanyId
updatedAt
}
}
Auth: JWT required.
The isSuperAdmin field is automatically stripped from update inputs to prevent privilege escalation. Setting email checks uniqueness against existing users.
Update behavior:
undefinedfields are ignored (not updated)nullvalues explicitly clear a field- Emits
user.profile.updateddomain event on success
Account Deletion
deleteMyAccount -- Self-Deletion
Any authenticated user can soft-delete their own account.
mutation DeleteMyAccount($input: DeleteMyAccountInput!) {
deleteMyAccount(input: $input) {
success
message
}
}
| Field | Type | Required | Description |
|---|---|---|---|
confirmDeletion | Boolean | Yes | Must be true |
reason | String | No | Optional reason for audit log |
Behavior:
- Always soft delete (
isActive = false), data preserved - SuperAdmin users cannot self-delete via this mutation
- Emits
user.account.deletedevent
Admin Deletion
Use the Admin API — adminDeleteUser and adminBulkDeleteUsers (both @RequireSuperAdmin) — for staff-driven deletion. They support soft delete (isActive = false, data preserved) and hard delete (transactional cascade with S3 cleanup).
A consumer-facing removeUser(id: ID!) mutation used to live on this page. It was guarded only by JwtAuthGuard — any authenticated user could pass any other user's id and trigger the full hard-delete cascade (CVs + S3 files, candidates, meetings, UCRs, notifications, the User row; FK-nulls on Talent / Invoice / InterviewBookingAttendee). The frontend never actually called it, so it was deleted from the schema rather than locked down — adminDeleteUser already covers the staff path and deleteMyAccount covers self-deletion.
Calling mutation { removeUser(id: "...") } against api.aiqlick.com now returns Cannot query field "removeUser" on type "Mutation". Same change of class as removeUserCompanyRole (SUP-00163), which was hardened rather than removed because it has live callers.
Company Membership
Company Talent Queries
query GetCompanyTalents($companyId: String!) {
companyTalents(companyId: $companyId) {
id
user {
id email firstName lastName
title expectedSalary
}
talentPool { id name }
createdAt
}
}
query GetCompanyTalentPools($companyId: String!) {
talentPoolsByCompany(companyId: $companyId) {
id
name
description
talents {
id
user { firstName lastName email }
}
}
}
Profile Sharing
External profile sharing uses UUID-based tokens with configurable expiration (default 7 days).
| Operation | Type | Description |
|---|---|---|
createUserProfileExternalShare | Mutation | Create a public shareable link for a user profile |
createUserProfileCompanyShare | Mutation | Share a user profile with a specific company |
createUserProfileUserShare | Mutation | Share a user profile with a specific user |
getUserProfileByPublicToken(token: String!) | Query | Access shared profile via public token |
removeUserProfileExternalShare(id: String!) | Mutation | Remove an external share link |
removeUserProfileCompanyShare(id: String!) | Mutation | Remove a company share |
removeUserProfileUserShare(id: String!) | Mutation | Remove a user share |
Share tokens are double-UUID format, track access count, and can be deactivated without deletion.
Error Handling
All user operations return structured GraphQL errors:
{
"message": "User not found",
"extensions": {
"code": "USER_NOT_FOUND",
"statusCode": 404,
"correlationId": "uuid",
"timestamp": "2026-01-15T10:30:00Z"
}
}
| Error Code | HTTP | When |
|---|---|---|
USER_NOT_FOUND | 404 | User ID does not exist |
USER_ALREADY_EXISTS | 409 | Duplicate email on create/update |
INVALID_CREDENTIALS | 401 | Wrong password on login |
EMAIL_NOT_VERIFIED | 403 | Login before email verification |
UNAUTHENTICATED | 401 | Missing or invalid JWT |
Events Emitted
User operations emit domain events consumed by notification listeners and activity loggers:
| Event | Trigger |
|---|---|
user.profile.updated | Profile fields changed |
user.account.deleted | Account soft/hard deleted |
user.email.verified | Email verification completed |
user.password.reset.requested | Password reset initiated |
user.2fa.enabled | Two-factor authentication toggled |
user.activation.invitation.sent | Verification email sent |