wacrm/src/components/settings/settings-sections.ts
Arnas Donauskas 456321a2f2
feat(api): public API groundwork — scoped API keys + /api/v1/me
Adds the machine-to-machine auth layer the public REST API needs so
external scripts and automations can drive wacrm (issue #245). This is
the groundwork release; data endpoints land one at a time on top of it.

- Migration 026: account-scoped `api_keys` table. SHA-256 hash at rest
  (plaintext shown once), display `key_prefix`, `scopes[]`, RLS via
  `is_account_member` (members read, admin+ manage).
- `src/lib/api-keys/`: key gen/hash (wacrm_live_ prefix, timing-safe),
  scope vocabulary, service-role auth-path store.
- `requireApiKey(request, scope?)` (src/lib/auth/api-context.ts):
  bearer parse -> hash lookup -> liveness -> per-key rate limit ->
  scopes-only authz. Returns a service-role-scoped account context.
- `/api/v1` envelope ({data} / {error:{code,message}}) + GET /api/v1/me
  reference endpoint that exercises the whole stack.
- Dashboard: admin+ key management routes + Settings -> API keys tab.
- Docs (docs/public-api.md), CHANGELOG, README, .env note.

Checks: typecheck clean, lint 0 errors, full suite green (+27 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:35:43 +03:00

82 lines
2.7 KiB
TypeScript

import {
Coins,
FileText,
KeyRound,
LayoutGrid,
Palette,
PlugZap,
Shield,
Tags,
User,
UsersRound,
type LucideIcon,
} from 'lucide-react';
/**
* Settings information architecture for the redesigned page.
*
* The flat tab strip became a grouped left rail with a new Overview
* landing. The URL query param stays `?tab=` (deep-linkable, and it
* keeps the existing links in sidebar.tsx / header.tsx working) — we
* just map the old values onto the new sections.
*/
export const SETTINGS_SECTIONS = [
'overview',
'profile',
'security',
'appearance',
'whatsapp',
'templates',
'fields',
'deals',
'members',
'api',
] as const;
export type SettingsSection = (typeof SETTINGS_SECTIONS)[number];
export const DEFAULT_SECTION: SettingsSection = 'overview';
/** Rail grouping. `adminOnly` items are hidden for non-admins. */
export interface SectionMeta {
id: SettingsSection;
label: string;
icon: LucideIcon;
group: 'top' | 'account' | 'workspace';
}
export const SECTION_META: Record<SettingsSection, SectionMeta> = {
overview: { id: 'overview', label: 'Overview', icon: LayoutGrid, group: 'top' },
profile: { id: 'profile', label: 'Your profile', icon: User, group: 'account' },
security: { id: 'security', label: 'Login & security', icon: Shield, group: 'account' },
appearance: { id: 'appearance', label: 'Appearance', icon: Palette, group: 'account' },
whatsapp: { id: 'whatsapp', label: 'WhatsApp', icon: PlugZap, group: 'workspace' },
templates: { id: 'templates', label: 'Templates', icon: FileText, group: 'workspace' },
fields: { id: 'fields', label: 'Fields & tags', icon: Tags, group: 'workspace' },
deals: { id: 'deals', label: 'Deals & currency', icon: Coins, group: 'workspace' },
members: { id: 'members', label: 'Team members', icon: UsersRound, group: 'workspace' },
api: { id: 'api', label: 'API keys', icon: KeyRound, group: 'workspace' },
};
export const RAIL_GROUPS: { label: string | null; group: SectionMeta['group'] }[] = [
{ label: null, group: 'top' },
{ label: 'Account', group: 'account' },
{ label: 'Workspace', group: 'workspace' },
];
function isSection(value: string | null): value is SettingsSection {
return !!value && (SETTINGS_SECTIONS as readonly string[]).includes(value);
}
/**
* Resolve a raw `?tab=` value to a section. Legacy tabs from the old
* flat layout collapse onto their new home (Tags + Custom fields → the
* merged "Fields & tags" section). Anything unknown falls back to the
* Overview landing.
*/
export function resolveSection(raw: string | null): SettingsSection {
if (raw === 'tags' || raw === 'custom-fields') return 'fields';
if (isSection(raw)) return raw;
return DEFAULT_SECTION;
}