Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Ishaq74/concordia/llms.txt

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

Overview

Concordia uses Better Auth for authentication. All endpoints are automatically generated and handled through /api/auth/[...all].

Base Endpoint

POST /api/auth/*
All authentication flows use the Better Auth SDK, which provides type-safe client methods.

Authentication Configuration

From /src/lib/auth/auth.ts:53-133:
const sharedConfig = {
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // 1 day
    freshAge: 60 * 10, // 10 minutes
    absoluteTimeout: 60 * 60 * 24 * 7, // 7 days
  },
  rateLimit: {
    enabled: true,
    storage: "database",
    windows: [
      { key: "global_ip", max: 100, window: 60 * 1000 },
      { key: "sign_in", max: 5, window: 15 * 60 * 1000 },
      { key: "sign_up", max: 10, window: 60 * 60 * 1000 },
    ],
  },
};

Registration

Sign Up

import { authClient } from '@/lib/auth-client';

const { data, error } = await authClient.signUp.email({
  email: 'user@example.com',
  password: 'SecurePass123!',
  name: 'John Doe',
  username: 'johndoe', // Optional
});

Validation Rules

From /src/lib/auth/auth.ts:57-64:
  • Email: Required, valid email format
  • Password:
    • Minimum 10 characters
    • At least 1 uppercase letter
    • At least 1 lowercase letter
    • At least 1 digit
    • At least 1 special character
  • Username: Optional, alphanumeric with hyphens/underscores
  • Name: Optional

Post-Signup Actions

From /src/lib/auth/auth.ts:196-266, after successful signup:
  1. Profile created with derived username
  2. Wallet initialized (balance: 0.00 EUR)
  3. Default role assigned (citizen)
  4. Audit log entry created
  5. Verification email sent (if SMTP configured)

Response

{
  "user": {
    "id": "usr_abc123",
    "email": "user@example.com",
    "name": "John Doe",
    "emailVerified": false,
    "createdAt": "2024-03-03T10:00:00Z"
  },
  "session": null // Email verification required
}

Email Verification

Send Verification Email

await authClient.sendVerificationEmail({
  email: 'user@example.com',
});

Verify Email

Users receive a verification link via email:
https://your-domain.com/verify-email?token=abc123...
The frontend calls:
await authClient.verifyEmail({
  token: 'abc123...',
});

Login

Sign In with Email/Password

const { data, error } = await authClient.signIn.email({
  email: 'user@example.com',
  password: 'SecurePass123!',
});

Rate Limiting

From /src/lib/auth/auth.ts:372-389:
  • Max attempts: 5 failed logins per email
  • Window: 15 minutes
  • Error: "Too many login attempts" (HTTP 429)

Response

{
  "user": {
    "id": "usr_abc123",
    "email": "user@example.com",
    "name": "John Doe",
    "emailVerified": true
  },
  "session": {
    "id": "ses_xyz789",
    "userId": "usr_abc123",
    "expiresAt": "2024-03-10T10:00:00Z"
  }
}

Audit Logging

From /src/lib/auth/auth.ts:290-306, successful logins are logged:
await db.insert(auditLog).values({
  action: "login_success",
  userId: session.userId,
  ip: extractedIP,
  userAgent: userAgent,
  data: { sessionId: session.id },
});
Failed logins also logged (login_failed).

Session Management

Get Current Session

const { data: session } = await authClient.getSession();

Response

{
  "session": {
    "id": "ses_xyz789",
    "userId": "usr_abc123",
    "expiresAt": "2024-03-10T10:00:00Z",
    "activeOrganizationId": "org_123"
  },
  "user": {
    "id": "usr_abc123",
    "email": "user@example.com",
    "name": "John Doe",
    "emailVerified": true
  }
}

Session Configuration

From /src/lib/auth/auth.ts:93-103:
expiresIn
number
default:"604800"
Session lifetime (7 days in seconds)
updateAge
number
default:"86400"
Session refresh interval (1 day)
Cookie cache duration (5 minutes)
freshAge
number
default:"600"
Fresh session window (10 minutes)
absoluteTimeout
number
default:"604800"
Maximum session lifetime (7 days)

Logout

Sign Out

await authClient.signOut();
From /src/lib/auth/auth.ts:338-346, Bearer tokens are invalidated on logout:
const origSignOut = (instance.api as any).signOut;
(instance.api as any).signOut = async (opts: any) => {
  const hdr = opts?.headers?.authorization;
  if (hdr && hdr.startsWith('Bearer ')) {
    invalidatedTokens.add(hdr.slice(7)); // Blacklist token
  }
  return await origSignOut(opts);
};

Password Reset

Request Password Reset

await authClient.forgetPassword({
  email: 'user@example.com',
  redirectTo: 'https://your-domain.com/reset-password',
});

Email Sent

From /src/lib/auth/auth.ts:65-76, users receive an email with reset link:
<p>Click the following link to reset your password:</p>
<p><a href="{resetUrl}">{resetUrl}</a></p>

Reset Password

await authClient.resetPassword({
  token: 'reset_token_123',
  password: 'NewSecurePass123!',
});

Organization Authentication

Set Active Organization

await authClient.organization.setActive({
  organizationId: 'org_123',
});
From /src/lib/auth/auth.ts:319, this is mapped to:
setActive: (payload: any) => (instance.api as any).setActiveOrganization(payload)

List User Organizations

const { data } = await authClient.organization.list();

Response

[
  {
    "id": "org_123",
    "name": "My Organization",
    "slug": "my-org",
    "role": "owner"
  }
]

Security Features

IP Tracking

From /src/lib/auth/auth.ts:16-23, IPs are extracted from:
function extractIP(ctx: any): string | null {
  return (
    ctx?.request?.headers?.get("x-real-ip") ||
    ctx?.request?.headers?.get("x-forwarded-for") ||
    ctx?.request?.headers?.get("cf-connecting-ip") ||
    null
  );
}

Secure Cookies

From /src/lib/auth/auth.ts:179-183:
advanced: {
  useSecureCookies: true,
  cookiePrefix: "astro_",
  crossSubDomainCookies: { enabled: false },
}

CSRF Protection

  • CSRF checks enabled by default
  • Trusted origins configured via BETTER_AUTH_URL

Error Responses

Common Errors

StatusErrorDescription
400invalid_inputInvalid email/password format
400weak_passwordPassword doesn’t meet requirements
401invalid_credentialsWrong email/password
401email_not_verifiedEmail verification required
403forbiddenAccess denied
429too_many_requestsRate limit exceeded
500internal_errorServer error

Example

{
  "message": "Invalid credentials",
  "status": "UNAUTHORIZED"
}

API Overview

Rate limiting and authentication basics

Better Auth Docs

Official Better Auth documentation

Build docs developers (and LLMs) love