Initial commit: TravelHub multi-tenant flight booking platform
Angular + Go + PostgreSQL sample with strict per-tenant isolation. - Backend: Go API (basic auth, tenant-scoped repository layer, pgx pool) - Frontend: Angular 22 SPA (search, checkout, bookings, dashboard) - Database: schema, seed data and demo bookings migrations - Tests: 311 across Go (integration), Angular (unit) and Playwright (e2e), plus a generated evidence report with PDF export - Security: whitebox VAPT report under security/ Secrets (.env, tokens, DB credentials) are git-ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/travelswitch/travelhub/backend/internal/models"
|
||||
)
|
||||
|
||||
// TenantBySlug resolves the tenant a caller is signing in to. Suspended
|
||||
// tenants are treated as absent so their users cannot authenticate.
|
||||
func (r *Repository) TenantBySlug(ctx context.Context, slug string) (models.Tenant, error) {
|
||||
const q = `
|
||||
SELECT id, slug, name, status, created_at
|
||||
FROM tenants
|
||||
WHERE slug = $1 AND status = 'active'`
|
||||
|
||||
var t models.Tenant
|
||||
err := r.pool.QueryRow(ctx, q, slug).
|
||||
Scan(&t.ID, &t.Slug, &t.Name, &t.Status, &t.CreatedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return models.Tenant{}, ErrNotFound
|
||||
}
|
||||
return t, err
|
||||
}
|
||||
|
||||
// ListTenants powers the tenant picker on the sign-in screen. It is the only
|
||||
// unauthenticated read in the service and deliberately exposes nothing beyond
|
||||
// the slug and display name.
|
||||
func (r *Repository) ListTenants(ctx context.Context) ([]models.Tenant, error) {
|
||||
const q = `
|
||||
SELECT id, slug, name, status, created_at
|
||||
FROM tenants
|
||||
WHERE status = 'active'
|
||||
ORDER BY name`
|
||||
|
||||
rows, err := r.pool.Query(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tenants := []models.Tenant{}
|
||||
for rows.Next() {
|
||||
var t models.Tenant
|
||||
if err := rows.Scan(&t.ID, &t.Slug, &t.Name, &t.Status, &t.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tenants = append(tenants, t)
|
||||
}
|
||||
return tenants, rows.Err()
|
||||
}
|
||||
|
||||
// UserByEmail looks up a login within one tenant. The tenant filter is what
|
||||
// stops credentials from one agency authenticating against another.
|
||||
func (r *Repository) UserByEmail(ctx context.Context, tenantID uuid.UUID, email string) (models.User, error) {
|
||||
const q = `
|
||||
SELECT id, tenant_id, email, password_hash, full_name, role, created_at
|
||||
FROM users
|
||||
WHERE tenant_id = $1 AND lower(email) = lower($2)`
|
||||
|
||||
var u models.User
|
||||
err := r.pool.QueryRow(ctx, q, tenantID, email).Scan(
|
||||
&u.ID, &u.TenantID, &u.Email, &u.PasswordHash, &u.FullName, &u.Role, &u.CreatedAt,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return models.User{}, ErrNotFound
|
||||
}
|
||||
return u, err
|
||||
}
|
||||
Reference in New Issue
Block a user