Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/elecodes/TenderCheck-AI/llms.txt

Use this file to discover all available pages before exploring further.

TenderCheck AI uses a hybrid authentication strategy: the server sets an HttpOnly token cookie as the primary session mechanism, and the same JWT is also returned in the response body so clients can store it as a Bearer token fallback. This dual approach exists because HttpOnly cookie reliability varies across platforms and CORS setups — native apps, server-to-server calls, and some cross-origin browser configurations cannot use cookies reliably, so the Authorization header path ensures those environments still work without any special server configuration. The authMiddleware always checks the cookie first. If no cookie is present it looks for an Authorization: Bearer <token> header. Either path leads to the same verified JWT payload.

Register

Create a new TenderCheck AI account.
POST /api/auth/register

Request Body

{
  "name": "Jane Doe",
  "email": "jane@example.com",
  "password": "Secure123!",
  "company": "Acme Corp"
}
name
string
required
Full display name. Minimum 2 characters.
email
string
required
A valid email address. Stored in lowercase.
password
string
required
Must satisfy all of the following rules (validated server-side via Zod):
  • Minimum 8 characters (PASSWORD_MIN_LENGTH = 8)
  • At least one uppercase letter (A–Z)
  • At least one number (0–9)
  • At least one special character (any non-alphanumeric character)
company
string
Optional company or organisation name.

Success Response — 201 Created

The server auto-logs the user in after registration: it issues a JWT, sets the HttpOnly cookie, and returns the token in the body.
{
  "message": "User registered successfully",
  "token": "<jwt_token>",
  "user": {
    "id": "a1b2c3d4-...",
    "email": "jane@example.com",
    "name": "Jane Doe",
    "company": "Acme Corp"
  }
}
message
string
Confirmation string: "User registered successfully".
token
string
Signed JWT. Store in localStorage as auth_token for the Bearer token fallback.
user
object
The newly created user record.
The token HttpOnly cookie is also set on the response automatically — no extra step needed in a browser context.

Error Responses

StatusCause
400Zod validation failure (password rules, invalid email, name too short)
500Email is already registered (duplicate account)
When a duplicate email is submitted, AuthService throws a plain Error("User already exists"). This is caught by the global error handler and returned as an HTTP 500 with the message "Something went wrong". User-facing code should treat any non-201 response from this endpoint as a registration failure and prompt the user to try a different email or log in instead.

Login

Authenticate an existing user with email and password.
POST /api/auth/login
This endpoint is rate-limited to 300 requests per 60-second window. Exceeding the limit returns HTTP 429. See Rate Limiting for details.

Request Body

{
  "email": "jane@example.com",
  "password": "Secure123!",
  "rememberMe": true
}
email
string
required
The account email address.
password
string
required
The account password.
rememberMe
boolean
When true, the token cookie is set with maxAge = 30 days. When false or omitted, the cookie is a session cookie that is cleared when the browser is closed.

Success Response — 200 OK

{
  "token": "<jwt_token>",
  "user": {
    "id": "a1b2c3d4-...",
    "email": "jane@example.com",
    "name": "Jane Doe",
    "company": "Acme Corp"
  }
}
The HttpOnly token cookie is set on the response in addition to the token field in the body.

Error Responses

StatusCause
400Invalid email or password format (Zod validation)
429Rate limit exceeded
500Incorrect credentials (email not found or password mismatch)
Incorrect credentials cause AuthService.login to throw a plain Error("Invalid credentials"), which the global error handler returns as HTTP 500 with "Something went wrong". This is an internal design detail — production error handling maps all unrecognised errors to a safe 500 to prevent credential enumeration.

Making Authenticated Requests

For protected endpoints, the frontend sends both the HttpOnly cookie and the Authorization header on every request. The backend accepts whichever is present.
const token = localStorage.getItem('auth_token');
const headers: HeadersInit = {
  'Content-Type': 'application/json',
};

if (token) {
  headers['Authorization'] = `Bearer ${token}`;  // Bearer token fallback
}

const response = await fetch(`${API_URL}/api/tenders`, {
  method: 'GET',
  headers,
  credentials: 'include', // Sends the HttpOnly cookie automatically
});
How the authMiddleware resolves the token:
  1. Checks req.cookies.token (HttpOnly cookie path).
  2. If no cookie, checks req.headers.authorization for a Bearer <token> string.
  3. If neither is present, responds with HTTP 401 "No authorization token provided".
  4. If a token is found but is invalid or expired, responds with HTTP 401 "Invalid or expired token".
For file upload endpoints (/api/tenders/analyze, /api/tenders/:id/validate-proposal), do not set a Content-Type header manually — fetch will set the correct multipart/form-data boundary automatically when you pass a FormData body. The Authorization header is still required if you’re not relying on cookies.

Get Current User

Retrieve the authenticated user’s profile. Use this on page load to restore an existing session without requiring the user to log in again.
GET /api/auth/me
Requires: Cookie or Authorization: Bearer <token> header.

Success Response — 200 OK

{
  "user": {
    "userId": "a1b2c3d4-..."
  },
  "token": "<cookie_token>"
}
user
object
The JWT payload decoded from the active token. Contains userId (the only claim signed into the token by AuthService).
token
string
The token currently stored in the HttpOnly cookie. Returned so the client can sync localStorage if it was cleared (e.g., after a hard refresh).
The user object returned by /api/auth/me contains only the claims that were signed into the JWT. AuthService signs tokens with { userId: user.id } only — email and name are not included in the JWT payload and are therefore not present in this response. To get the full user profile (including name and email), use the data returned from /api/auth/login or /api/auth/register and cache it client-side.

Error Responses

StatusCause
401No token or invalid/expired token

Logout

Clear the server-side session cookie.
POST /api/auth/logout
No request body is required.

Success Response — 200 OK

{
  "message": "Logged out successfully"
}
The token HttpOnly cookie is cleared with clearCookie. The frontend should also remove the localStorage token immediately after a successful logout:
await fetch(`${API_URL}/api/auth/logout`, {
  method: 'POST',
  credentials: 'include',
});

localStorage.removeItem('auth_token');
localStorage.removeItem('user');

Google OAuth (PKCE)

Sign in or register using a Google account via the Authorization Code + PKCE flow.
POST /api/auth/google/callback
This endpoint is rate-limited to 300 requests per 60-second window — the same limiter as /api/auth/login. See Rate Limiting for details.

Flow

1

Initiate on the frontend

The frontend generates a PKCE codeVerifier and codeChallenge pair, then redirects the user to Google’s authorisation endpoint with response_type=code and the code_challenge.
2

Google redirects back

After the user consents, Google redirects back to the frontend with a one-time code in the URL query string.
3

Send code + verifier to backend

The frontend POSTs the code and the original codeVerifier (never exposed to Google) to /api/auth/google/callback.
4

Backend exchanges and issues session

The backend exchanges the code for Google tokens, extracts user info from the ID token, finds or creates the user in the database, and returns a TenderCheck AI JWT + sets the HttpOnly cookie.

Request Body

{
  "code": "<authorization_code_from_google>",
  "codeVerifier": "<pkce_code_verifier>"
}
code
string
required
The one-time authorization code returned by Google’s OAuth consent screen.
codeVerifier
string
required
The PKCE code verifier generated by the frontend before initiating the OAuth flow. Never sent to Google directly — only to this endpoint.

Success Response — 200 OK

{
  "token": "<jwt_token>",
  "user": {
    "id": "a1b2c3d4-...",
    "email": "jane@example.com",
    "name": "Jane Doe",
    "company": null
  }
}
The HttpOnly cookie is also set with rememberMe = true (30-day maxAge) for Google logins.

Error Responses

StatusCause
400Missing or malformed code / codeVerifier fields
400Google account does not have an associated email
401Google code exchange failed (expired or already used code)
429Rate limit exceeded
500GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET not configured on the server

Required Backend Environment Variables

GOOGLE_CLIENT_ID=your_google_oauth_client_id
GOOGLE_CLIENT_SECRET=your_google_oauth_client_secret
Why PKCE? The PKCE (Proof Key for Code Exchange) extension prevents authorization code interception attacks. The codeVerifier is a cryptographic secret known only to the initiating client — even if an attacker intercepts the code in the redirect URL, they cannot exchange it without the matching verifier. This is why TenderCheck AI uses the Authorization Code + PKCE flow rather than the legacy Implicit flow (which exposed access tokens directly in the URL).

Password Reset

Request a password reset link for an account.
POST /api/auth/reset-password-request

Request Body

{
  "email": "jane@example.com"
}
email
string
required
The email address associated with the account.

Success Response — 200 OK

{
  "message": "If email exists, instructions have been sent."
}
This endpoint always returns HTTP 200, regardless of whether the email is registered. This is intentional — it prevents user enumeration attacks where an attacker could determine which email addresses have accounts by probing the API.

Error Responses

StatusCause
400Invalid email format

JWT Token Details

All JWTs are signed and verified using the JWT_SECRET environment variable.
# .env (backend)
JWT_SECRET=your_long_random_secret_string
PropertyValue
Signing algorithmHS256 (jsonwebtoken default)
Default expiry1d (1 day, from AuthService)
Session cookie (no rememberMe)Cookie cleared on browser close; token itself valid for 1 day
Persistent cookie (rememberMe: true)Cookie maxAge = 30 days
JWT payload{ userId: string }
The JWT is signed with only the userId claim:
jwt.sign(
  { userId: user.id },          // only claim in the token
  process.env.JWT_SECRET,
  { expiresIn: "1d" }
);
The authMiddleware attaches the decoded payload to req.user with the following TypeScript shape — note that email and role will only be populated if they were signed into the token by a future version of the service:
req.user = {
  userId: string;
  email: string;   // currently not signed into the JWT
  role?: string;   // currently not signed into the JWT
}
Storing the token in localStorage is supported as a fallback, but it exposes the token to JavaScript running on the page (XSS risk). Prefer the HttpOnly cookie path wherever possible — it is inaccessible to JavaScript and is sent automatically on every same-origin (or credentialed cross-origin) request. Only use localStorage as a secondary fallback for environments where cookies cannot be used.

Build docs developers (and LLMs) love