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>
73 lines
2.1 KiB
Go
73 lines
2.1 KiB
Go
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
|
|
}
|