flarecms 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth/index.js +201 -1
  3. package/dist/cli/commands.js +5554 -55
  4. package/dist/cli/index.js +5554 -55
  5. package/dist/cli/mcp.js +30 -0
  6. package/dist/client/index.js +23576 -0
  7. package/dist/db/index.js +10392 -25
  8. package/dist/index.js +56776 -7582
  9. package/dist/server/index.js +43280 -0
  10. package/dist/style.css +5536 -0
  11. package/package.json +33 -30
  12. package/scripts/fix-api-paths.mjs +0 -32
  13. package/scripts/fix-imports.mjs +0 -38
  14. package/scripts/prefix-css.mjs +0 -45
  15. package/src/api/lib/cache.ts +0 -45
  16. package/src/api/lib/response.ts +0 -40
  17. package/src/api/middlewares/auth.ts +0 -186
  18. package/src/api/middlewares/cors.ts +0 -10
  19. package/src/api/middlewares/rbac.ts +0 -85
  20. package/src/api/routes/auth.ts +0 -377
  21. package/src/api/routes/collections.ts +0 -205
  22. package/src/api/routes/content.ts +0 -175
  23. package/src/api/routes/device.ts +0 -160
  24. package/src/api/routes/magic.ts +0 -150
  25. package/src/api/routes/mcp.ts +0 -273
  26. package/src/api/routes/oauth.ts +0 -160
  27. package/src/api/routes/settings.ts +0 -43
  28. package/src/api/routes/setup.ts +0 -307
  29. package/src/api/routes/tokens.ts +0 -80
  30. package/src/api/schemas/auth.ts +0 -15
  31. package/src/api/schemas/index.ts +0 -51
  32. package/src/api/schemas/tokens.ts +0 -24
  33. package/src/auth/index.ts +0 -28
  34. package/src/cli/commands.ts +0 -217
  35. package/src/cli/index.ts +0 -21
  36. package/src/cli/mcp.ts +0 -210
  37. package/src/cli/tests/cli.test.ts +0 -40
  38. package/src/cli/tests/create.test.ts +0 -87
  39. package/src/client/FlareAdminRouter.tsx +0 -47
  40. package/src/client/app.tsx +0 -175
  41. package/src/client/components/app-sidebar.tsx +0 -227
  42. package/src/client/components/collection-modal.tsx +0 -215
  43. package/src/client/components/content-list.tsx +0 -247
  44. package/src/client/components/dynamic-form.tsx +0 -190
  45. package/src/client/components/field-modal.tsx +0 -221
  46. package/src/client/components/settings/api-token-section.tsx +0 -400
  47. package/src/client/components/settings/general-section.tsx +0 -224
  48. package/src/client/components/settings/security-section.tsx +0 -154
  49. package/src/client/components/settings/seo-section.tsx +0 -200
  50. package/src/client/components/settings/signup-section.tsx +0 -257
  51. package/src/client/components/ui/accordion.tsx +0 -78
  52. package/src/client/components/ui/avatar.tsx +0 -107
  53. package/src/client/components/ui/badge.tsx +0 -52
  54. package/src/client/components/ui/button.tsx +0 -60
  55. package/src/client/components/ui/card.tsx +0 -103
  56. package/src/client/components/ui/checkbox.tsx +0 -27
  57. package/src/client/components/ui/collapsible.tsx +0 -19
  58. package/src/client/components/ui/dialog.tsx +0 -162
  59. package/src/client/components/ui/icon-picker.tsx +0 -485
  60. package/src/client/components/ui/icons-data.ts +0 -8476
  61. package/src/client/components/ui/input.tsx +0 -20
  62. package/src/client/components/ui/label.tsx +0 -20
  63. package/src/client/components/ui/popover.tsx +0 -91
  64. package/src/client/components/ui/select.tsx +0 -204
  65. package/src/client/components/ui/separator.tsx +0 -23
  66. package/src/client/components/ui/sheet.tsx +0 -141
  67. package/src/client/components/ui/sidebar.tsx +0 -722
  68. package/src/client/components/ui/skeleton.tsx +0 -13
  69. package/src/client/components/ui/sonner.tsx +0 -47
  70. package/src/client/components/ui/switch.tsx +0 -30
  71. package/src/client/components/ui/table.tsx +0 -116
  72. package/src/client/components/ui/tabs.tsx +0 -80
  73. package/src/client/components/ui/textarea.tsx +0 -18
  74. package/src/client/components/ui/tooltip.tsx +0 -68
  75. package/src/client/hooks/use-mobile.ts +0 -19
  76. package/src/client/index.css +0 -149
  77. package/src/client/index.ts +0 -7
  78. package/src/client/layouts/admin-layout.tsx +0 -93
  79. package/src/client/layouts/settings-layout.tsx +0 -104
  80. package/src/client/lib/api.ts +0 -72
  81. package/src/client/lib/utils.ts +0 -6
  82. package/src/client/main.tsx +0 -10
  83. package/src/client/pages/collection-detail.tsx +0 -634
  84. package/src/client/pages/collections.tsx +0 -180
  85. package/src/client/pages/dashboard.tsx +0 -133
  86. package/src/client/pages/device.tsx +0 -66
  87. package/src/client/pages/document-detail-page.tsx +0 -139
  88. package/src/client/pages/documents-page.tsx +0 -103
  89. package/src/client/pages/login.tsx +0 -345
  90. package/src/client/pages/settings.tsx +0 -65
  91. package/src/client/pages/setup.tsx +0 -129
  92. package/src/client/pages/signup.tsx +0 -188
  93. package/src/client/store/auth.ts +0 -30
  94. package/src/client/store/collections.ts +0 -13
  95. package/src/client/store/config.ts +0 -12
  96. package/src/client/store/fetcher.ts +0 -30
  97. package/src/client/store/router.ts +0 -95
  98. package/src/client/store/schema.ts +0 -39
  99. package/src/client/store/settings.ts +0 -31
  100. package/src/client/types.ts +0 -34
  101. package/src/db/dynamic.ts +0 -70
  102. package/src/db/index.ts +0 -16
  103. package/src/db/migrations/001_initial_schema.ts +0 -57
  104. package/src/db/migrations/002_auth_tables.ts +0 -84
  105. package/src/db/migrator.ts +0 -61
  106. package/src/db/schema.ts +0 -142
  107. package/src/index.ts +0 -12
  108. package/src/server/index.ts +0 -66
  109. package/src/types.ts +0 -20
  110. package/tests/css.test.ts +0 -21
  111. package/tests/modular.test.ts +0 -29
  112. package/tsconfig.json +0 -10
  113. /package/{style.css.d.ts → dist/style.css.d.ts} +0 -0
@@ -1,154 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { apiFetch } from '../../lib/api';
3
- import { startRegistration } from '@simplewebauthn/browser';
4
- import { Button } from '../ui/button';
5
- import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/card';
6
- import { Badge } from '../ui/badge';
7
- import { Loader2Icon, ShieldCheckIcon, PlusIcon, MonitorIcon, SmartphoneIcon, Trash2Icon } from 'lucide-react';
8
-
9
- export function SecuritySection() {
10
- const [passkeys, setPasskeys] = useState<any[]>([]);
11
- const [loading, setLoading] = useState(true);
12
- const [registering, setRegistering] = useState(false);
13
- const [error, setError] = useState('');
14
- const [passkeyName, setPasskeyName] = useState('');
15
-
16
- const fetchPasskeys = async () => {
17
- const res = await apiFetch('/auth/passkeys');
18
- if (res.ok) {
19
- const result = await res.json();
20
- setPasskeys(result.data);
21
- }
22
- setLoading(false);
23
- };
24
-
25
- useEffect(() => {
26
- fetchPasskeys();
27
- }, []);
28
-
29
- const handleAddPasskey = async () => {
30
- setRegistering(true);
31
- setError('');
32
- try {
33
- // 1. Get options from backend
34
- const optionsRes = await apiFetch('/auth/passkey/register/options', { method: 'POST' });
35
- if (!optionsRes.ok) throw new Error('Failed to get registration options');
36
- const optionsData = await optionsRes.json();
37
- const options = optionsData.data;
38
-
39
- // 2. Start registration on client
40
- const attestationResponse = await startRegistration(options);
41
-
42
- // 3. Verify on backend (include name)
43
- const verifyRes = await apiFetch('/auth/passkey/register/verify', {
44
- method: 'POST',
45
- body: JSON.stringify({
46
- ...attestationResponse,
47
- name: passkeyName || undefined
48
- }),
49
- });
50
-
51
- if (verifyRes.ok) {
52
- setPasskeyName('');
53
- fetchPasskeys();
54
- } else {
55
- const errorData = await verifyRes.json();
56
- throw new Error(errorData.error || 'Verification failed');
57
- }
58
- } catch (err: any) {
59
- setError(err.message);
60
- } finally {
61
- setRegistering(false);
62
- }
63
- };
64
-
65
- const handleDelete = async (pkId: string) => {
66
- if (!confirm('Revoke this security key? You will no longer be able to use it to sign in.')) return;
67
- const res = await apiFetch(`/auth/passkey/${pkId}`, { method: 'DELETE' });
68
- if (res.ok) fetchPasskeys();
69
- };
70
-
71
- if (loading) return <div className="flex items-center gap-2 text-muted-foreground"><Loader2Icon className="size-4 animate-spin" /> Loading security profile...</div>;
72
-
73
- return (
74
- <div className="space-y-12">
75
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
76
- <div>
77
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">Passkeys</h3>
78
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">Passkeys provide a high-security, passwordless authentication experience using biometric or hardware-bound keys.</p>
79
- </div>
80
- <div className="md:col-span-2 space-y-6">
81
- <div className="space-y-4">
82
- {passkeys.map((pk) => (
83
- <div key={pk.id} className="bg-card border rounded-xl p-5 flex items-center justify-between group hover:border-primary/20 transition-colors">
84
- <div className="flex items-center gap-4">
85
- <div className="size-10 bg-muted/50 rounded-lg flex items-center justify-center border text-muted-foreground group-hover:text-primary transition-colors">
86
- {pk.device_type === 'singleDevice' ? <SmartphoneIcon className="size-5" /> : <MonitorIcon className="size-5" />}
87
- </div>
88
- <div>
89
- <div className="flex items-center gap-2">
90
- <p className="text-sm font-bold text-foreground">
91
- {pk.name || (pk.device_type === 'singleDevice' ? 'Handheld Decryptor' : 'Workstation Bound')}
92
- </p>
93
- <Badge variant="secondary" className="text-[9px] uppercase font-bold tracking-tighter h-4 px-1 leading-none">Secure</Badge>
94
- </div>
95
- <div className="flex items-center gap-2 mt-1">
96
- <span className="text-[10px] text-muted-foreground font-medium font-mono opacity-50 uppercase">{pk.id.substring(0, 16)}...</span>
97
- <span className="text-[10px] text-muted-foreground/30">•</span>
98
- <span className="text-[10px] text-muted-foreground font-medium uppercase tracking-tighter">
99
- Last active: {pk.last_used_at ? new Date(pk.last_used_at).toLocaleDateString() : 'Never'}
100
- </span>
101
- </div>
102
- </div>
103
- </div>
104
- <Button
105
- variant="ghost"
106
- size="icon"
107
- className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-destructive"
108
- onClick={() => handleDelete(pk.id)}
109
- >
110
- <Trash2Icon className="size-4" />
111
- </Button>
112
- </div>
113
- ))}
114
-
115
- {passkeys.length === 0 && (
116
- <div className="bg-muted/10 border border-dashed rounded-xl p-12 text-center flex flex-col items-center justify-center">
117
- <ShieldCheckIcon className="size-8 text-muted-foreground/30 mb-4" />
118
- <p className="text-sm font-semibold text-muted-foreground">No passkeys registered</p>
119
- <p className="text-[10px] text-muted-foreground/50 uppercase tracking-widest mt-1">Protect your account with modern credentials</p>
120
- </div>
121
- )}
122
- </div>
123
-
124
- <Button
125
- onClick={handleAddPasskey}
126
- disabled={registering}
127
- className="w-full h-12 gap-2 font-bold uppercase tracking-widest text-[10px] transition-all hover:shadow-lg hover:shadow-primary/10"
128
- >
129
- {registering ? <Loader2Icon className="size-3 animate-spin" /> : <PlusIcon className="size-4" />}
130
- Register New Security Key
131
- </Button>
132
-
133
- {error && <p className="text-[11px] text-destructive font-medium text-center">{error}</p>}
134
- </div>
135
- </div>
136
-
137
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 border-t pt-12">
138
- <div>
139
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">Advanced Protection</h3>
140
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">Additional controls to manage session persistence and security levels.</p>
141
- </div>
142
- <div className="md:col-span-2 space-y-4">
143
- {/* Placeholder for more security settings */}
144
- <div className="bg-muted/5 border rounded-lg p-4 opacity-50 cursor-not-allowed">
145
- <div className="flex items-center justify-between">
146
- <span className="text-xs font-bold uppercase tracking-widest">Multi-Factor Enforcement</span>
147
- <div className="size-4 rounded-full border border-muted-foreground/30" />
148
- </div>
149
- </div>
150
- </div>
151
- </div>
152
- </div>
153
- );
154
- }
@@ -1,200 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { api } from '../../lib/api';
3
- import { Button } from '../ui/button';
4
- import { Input } from '../ui/input';
5
- import { Label } from '../ui/label';
6
- import { Textarea } from '../ui/textarea';
7
- import { Loader2Icon, SaveIcon, Share2Icon } from 'lucide-react';
8
- import { updateSettingsLocally } from '../../store/settings';
9
-
10
- export function SEOSection() {
11
- const [settings, setSettings] = useState<any>({});
12
- const [loading, setLoading] = useState(true);
13
- const [saving, setSaving] = useState(false);
14
-
15
- useEffect(() => {
16
- const fetchSettings = async () => {
17
- try {
18
- const data = await api.get('/settings').json<any>();
19
- setSettings(data);
20
- } catch (err) {}
21
- setLoading(false);
22
- };
23
- fetchSettings();
24
- }, []);
25
-
26
- const handleSave = async (e?: React.FormEvent) => {
27
- if (e) e.preventDefault();
28
- setSaving(true);
29
- try {
30
- await api.patch('/api/settings', { json: settings });
31
- updateSettingsLocally(settings);
32
- } catch (err) {}
33
- setSaving(false);
34
- };
35
-
36
- const update = (key: string, value: string) => {
37
- setSettings((prev: any) => ({ ...prev, [key]: value }));
38
- };
39
-
40
- if (loading)
41
- return (
42
- <div className="flex items-center gap-2 text-muted-foreground">
43
- <Loader2Icon className="size-4 animate-spin" /> Retrieving SEO
44
- architecture...
45
- </div>
46
- );
47
-
48
- return (
49
- <div className="space-y-12">
50
- <form onSubmit={handleSave} className="space-y-12">
51
- {/* Indexing & Search */}
52
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
53
- <div>
54
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
55
- Indexing & Search
56
- </h3>
57
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
58
- Configure how search engines discover and rank your content.
59
- </p>
60
- </div>
61
- <div className="md:col-span-2 space-y-6 bg-card border rounded-xl p-8 shadow-sm">
62
- <div className="grid gap-6">
63
- <div className="grid gap-2">
64
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
65
- Global Meta Description
66
- </Label>
67
- <Textarea
68
- value={settings['flare:seo_meta_description'] || ''}
69
- onChange={(e) =>
70
- update('flare:seo_meta_description', e.target.value)
71
- }
72
- placeholder="The definitive guide to your platform..."
73
- className="min-h-[100px] resize-none"
74
- />
75
- <p className="text-[9px] text-muted-foreground italic">
76
- Fallback description for pages without explicit meta content.
77
- </p>
78
- </div>
79
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
80
- <div className="grid gap-2">
81
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
82
- Google Verification
83
- </Label>
84
- <Input
85
- value={settings['flare:seo_google_verification'] || ''}
86
- onChange={(e) =>
87
- update('flare:seo_google_verification', e.target.value)
88
- }
89
- placeholder="xv...7"
90
- />
91
- </div>
92
- <div className="grid gap-2">
93
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
94
- Bing Verification
95
- </Label>
96
- <Input
97
- value={settings['flare:seo_bing_verification'] || ''}
98
- onChange={(e) =>
99
- update('flare:seo_bing_verification', e.target.value)
100
- }
101
- placeholder="89...A"
102
- />
103
- </div>
104
- </div>
105
- </div>
106
- </div>
107
- </div>
108
-
109
- {/* Social Presence */}
110
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 border-t pt-12">
111
- <div>
112
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
113
- Social Presence
114
- </h3>
115
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
116
- Override how your content appears when shared across social
117
- networks.
118
- </p>
119
- </div>
120
- <div className="md:col-span-2 space-y-6 bg-card border rounded-xl p-8 shadow-sm">
121
- <div className="grid gap-6">
122
- <div className="grid gap-2">
123
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50 flex items-center gap-2">
124
- <Share2Icon className="size-3 text-primary" />
125
- Default Social Share Image (OG)
126
- </Label>
127
- <Input
128
- value={settings['flare:seo_og_image'] || ''}
129
- onChange={(e) => update('flare:seo_og_image', e.target.value)}
130
- placeholder="https://cdn.com/og-default.jpg"
131
- />
132
- <p className="text-[9px] text-muted-foreground italic">
133
- Recommended size: 1200x630px.
134
- </p>
135
- </div>
136
- <div className="grid gap-2">
137
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
138
- Twitter (@username)
139
- </Label>
140
- <Input
141
- value={settings['flare:seo_twitter_handle'] || ''}
142
- onChange={(e) =>
143
- update('flare:seo_twitter_handle', e.target.value)
144
- }
145
- placeholder="@flarecms"
146
- />
147
- </div>
148
- </div>
149
- </div>
150
- </div>
151
-
152
- {/* Advanced SEO */}
153
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 border-t pt-12">
154
- <div>
155
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
156
- Sitemap & Discovery
157
- </h3>
158
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
159
- Core automation handles for link integrity and crawling.
160
- </p>
161
- </div>
162
- <div className="md:col-span-2 space-y-4">
163
- <div className="bg-card border rounded-xl p-8 shadow-sm space-y-6">
164
- <div className="grid gap-2">
165
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
166
- Robots.txt Payload
167
- </Label>
168
- <Textarea
169
- value={
170
- settings['flare:seo_robots_txt'] ||
171
- 'User-agent: *\nAllow: /'
172
- }
173
- onChange={(e) =>
174
- update('flare:seo_robots_txt', e.target.value)
175
- }
176
- className="min-h-[120px] font-mono text-[11px]"
177
- />
178
- </div>
179
- </div>
180
- </div>
181
- </div>
182
-
183
- <div className="flex justify-end sticky bottom-0 py-4 bg-background/80 backdrop-blur-sm border-t mt-12">
184
- <Button
185
- disabled={saving}
186
- size="sm"
187
- className="gap-2 font-bold uppercase tracking-widest text-[10px] h-10 px-6"
188
- >
189
- {saving ? (
190
- <Loader2Icon className="size-3 animate-spin" />
191
- ) : (
192
- <SaveIcon className="size-3" />
193
- )}
194
- Sync SEO Schema
195
- </Button>
196
- </div>
197
- </form>
198
- </div>
199
- );
200
- }
@@ -1,257 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { api } from '../../lib/api';
3
- import { Button } from '../ui/button';
4
- import { Input } from '../ui/input';
5
- import { Label } from '../ui/label';
6
- import {
7
- Select,
8
- SelectContent,
9
- SelectItem,
10
- SelectTrigger,
11
- SelectValue,
12
- } from '../ui/select';
13
- import { Switch } from '../ui/switch';
14
- import {
15
- Loader2Icon,
16
- SaveIcon,
17
- XIcon,
18
- ShieldIcon,
19
- GlobeIcon,
20
- UserCheckIcon,
21
- } from 'lucide-react';
22
- import { updateSettingsLocally } from '../../store/settings';
23
-
24
- export function SignupSection() {
25
- const [settings, setSettings] = useState<any>({});
26
- const [loading, setLoading] = useState(true);
27
- const [saving, setSaving] = useState(false);
28
- const [newDomain, setNewDomain] = useState('');
29
- const [newDomainRole, setNewDomainRole] = useState('viewer');
30
-
31
- useEffect(() => {
32
- const fetchSettings = async () => {
33
- try {
34
- const data = await api.get('/settings').json<any>();
35
- setSettings(data);
36
- } catch (err) {
37
- console.error('Failed to load settings:', err);
38
- } finally {
39
- setLoading(false);
40
- }
41
- };
42
- fetchSettings();
43
- }, []);
44
-
45
- const handleSave = async (e?: React.FormEvent) => {
46
- if (e) e.preventDefault();
47
- setSaving(true);
48
- try {
49
- await api.patch('/api/settings', { json: settings });
50
- updateSettingsLocally(settings);
51
- } catch (err) {
52
- console.error('Failed to save settings:', err);
53
- } finally {
54
- setSaving(false);
55
- }
56
- };
57
-
58
- const update = (key: string, value: string) => {
59
- setSettings((prev: any) => ({ ...prev, [key]: value }));
60
- };
61
-
62
- const domainRules = JSON.parse(
63
- settings['flare:signup_domain_rules'] || '{}',
64
- ) as Record<string, string>;
65
-
66
- const addDomainRule = () => {
67
- if (!newDomain) return;
68
- const newRules = { ...domainRules, [newDomain]: newDomainRole };
69
- update('flare:signup_domain_rules', JSON.stringify(newRules));
70
- setNewDomain('');
71
- };
72
-
73
- const removeDomainRule = (domain: string) => {
74
- const newRules = { ...domainRules };
75
- delete newRules[domain];
76
- update('flare:signup_domain_rules', JSON.stringify(newRules));
77
- };
78
-
79
- if (loading)
80
- return (
81
- <div className="flex items-center gap-2 text-muted-foreground">
82
- <Loader2Icon className="size-4 animate-spin" /> Loading
83
- configurations...
84
- </div>
85
- );
86
-
87
- return (
88
- <div className="space-y-12">
89
- <form onSubmit={handleSave} className="space-y-8">
90
- {/* Registration Toggle */}
91
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
92
- <div>
93
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
94
- Access Gate
95
- </h3>
96
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
97
- Determine if new users can register themselves or if the system
98
- remains invite-only.
99
- </p>
100
- </div>
101
- <div className="md:col-span-2 bg-card border rounded-xl p-8 shadow-sm">
102
- <div className="flex items-center justify-between">
103
- <div className="space-y-1">
104
- <p className="text-sm font-bold text-foreground">
105
- Self-Registration Enabled
106
- </p>
107
- <p className="text-[10px] text-muted-foreground/60">
108
- If disabled, all signups will be rejected even from valid
109
- domains.
110
- </p>
111
- </div>
112
- <Switch
113
- checked={settings['flare:signup_enabled'] === 'true'}
114
- onCheckedChange={(checked: boolean) =>
115
- update('flare:signup_enabled', String(checked))
116
- }
117
- />
118
- </div>
119
- </div>
120
- </div>
121
-
122
- {/* Domain Policies */}
123
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 border-t pt-8">
124
- <div>
125
- <h3 className="text-sm font-bold uppercase tracking-widest text-foreground">
126
- Domain Intelligence
127
- </h3>
128
- <p className="text-xs text-muted-foreground mt-2 leading-relaxed">
129
- Automate role assignment based on the user's verified email
130
- domain.
131
- </p>
132
- </div>
133
- <div className="md:col-span-2 space-y-6 bg-card border rounded-xl p-8 shadow-sm">
134
- <div className="space-y-6">
135
- <div className="grid gap-4">
136
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50">
137
- Provisioning Rules
138
- </Label>
139
- <div className="flex flex-col sm:flex-row gap-3">
140
- <div className="flex-1 relative">
141
- <GlobeIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground/40" />
142
- <Input
143
- value={newDomain}
144
- onChange={(e) => setNewDomain(e.target.value)}
145
- placeholder="company.com"
146
- className="pl-10 h-10"
147
- />
148
- </div>
149
- <Select
150
- value={newDomainRole}
151
- onValueChange={(v) => setNewDomainRole(v || 'viewer')}
152
- >
153
- <SelectTrigger className="w-full sm:w-[130px] h-10 font-bold text-[10px] uppercase">
154
- <SelectValue />
155
- </SelectTrigger>
156
- <SelectContent>
157
- <SelectItem value="admin">Admin</SelectItem>
158
- <SelectItem value="editor">Editor</SelectItem>
159
- <SelectItem value="viewer">Viewer</SelectItem>
160
- </SelectContent>
161
- </Select>
162
- <Button
163
- type="button"
164
- variant="secondary"
165
- onClick={addDomainRule}
166
- className="h-10 uppercase font-black text-[10px] tracking-widest px-6 shrink-0"
167
- >
168
- Map Rule
169
- </Button>
170
- </div>
171
-
172
- <div className="space-y-2 mt-2">
173
- {Object.entries(domainRules).map(([domain, role]) => (
174
- <div
175
- key={domain}
176
- className="flex items-center justify-between p-3 rounded-lg border bg-muted/20 group"
177
- >
178
- <div className="flex items-center gap-3">
179
- <div className="size-8 rounded bg-background border flex items-center justify-center text-muted-foreground shadow-sm">
180
- <GlobeIcon className="size-4" />
181
- </div>
182
- <div>
183
- <p className="text-xs font-bold text-foreground">
184
- @{domain}
185
- </p>
186
- <p className="text-[10px] text-muted-foreground uppercase font-black tracking-widest opacity-50">
187
- Maps to {role}
188
- </p>
189
- </div>
190
- </div>
191
- <Button
192
- type="button"
193
- variant="ghost"
194
- size="icon"
195
- onClick={() => removeDomainRule(domain)}
196
- className="size-8 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
197
- >
198
- <XIcon className="size-3.5" />
199
- </Button>
200
- </div>
201
- ))}
202
- {Object.keys(domainRules).length === 0 && (
203
- <div className="text-center py-6 border-2 border-dashed rounded-xl opacity-20">
204
- <p className="text-[10px] font-black uppercase tracking-[0.2em]">
205
- No custom rules defined
206
- </p>
207
- </div>
208
- )}
209
- </div>
210
- </div>
211
-
212
- <div className="grid gap-2 max-w-[240px] pt-4 border-t border-border/50">
213
- <Label className="text-[10px] font-black uppercase tracking-widest opacity-50 flex items-center gap-2">
214
- <UserCheckIcon className="size-3 text-primary opacity-50" />
215
- Fallback Default Role
216
- </Label>
217
- <Select
218
- value={settings['flare:signup_default_role'] || 'viewer'}
219
- onValueChange={(v: string) =>
220
- update('flare:signup_default_role', v)
221
- }
222
- >
223
- <SelectTrigger className="h-10 font-bold uppercase text-[10px] tracking-widest">
224
- <SelectValue placeholder="Select Role" />
225
- </SelectTrigger>
226
- <SelectContent>
227
- <SelectItem value="admin">Admin</SelectItem>
228
- <SelectItem value="editor">Editor</SelectItem>
229
- <SelectItem value="viewer">Viewer</SelectItem>
230
- </SelectContent>
231
- </Select>
232
- <p className="text-[10px] text-muted-foreground/60 italic leading-tight mt-1">
233
- Assigned to valid signups not matching specific domain rules.
234
- </p>
235
- </div>
236
- </div>
237
- </div>
238
- </div>
239
-
240
- <div className="flex justify-end sticky bottom-0 py-4 bg-background/80 backdrop-blur-sm border-t mt-12">
241
- <Button
242
- disabled={saving}
243
- size="sm"
244
- className="gap-2 font-bold uppercase tracking-widest text-[10px] h-10 px-6"
245
- >
246
- {saving ? (
247
- <Loader2Icon className="size-3 animate-spin" />
248
- ) : (
249
- <SaveIcon className="size-3" />
250
- )}
251
- Sync Access Policy
252
- </Button>
253
- </div>
254
- </form>
255
- </div>
256
- );
257
- }