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.
- package/LICENSE +21 -0
- package/dist/auth/index.js +201 -1
- package/dist/cli/commands.js +5554 -55
- package/dist/cli/index.js +5554 -55
- package/dist/cli/mcp.js +30 -0
- package/dist/client/index.js +23576 -0
- package/dist/db/index.js +10392 -25
- package/dist/index.js +56776 -7582
- package/dist/server/index.js +43280 -0
- package/dist/style.css +5536 -0
- package/package.json +33 -30
- package/scripts/fix-api-paths.mjs +0 -32
- package/scripts/fix-imports.mjs +0 -38
- package/scripts/prefix-css.mjs +0 -45
- package/src/api/lib/cache.ts +0 -45
- package/src/api/lib/response.ts +0 -40
- package/src/api/middlewares/auth.ts +0 -186
- package/src/api/middlewares/cors.ts +0 -10
- package/src/api/middlewares/rbac.ts +0 -85
- package/src/api/routes/auth.ts +0 -377
- package/src/api/routes/collections.ts +0 -205
- package/src/api/routes/content.ts +0 -175
- package/src/api/routes/device.ts +0 -160
- package/src/api/routes/magic.ts +0 -150
- package/src/api/routes/mcp.ts +0 -273
- package/src/api/routes/oauth.ts +0 -160
- package/src/api/routes/settings.ts +0 -43
- package/src/api/routes/setup.ts +0 -307
- package/src/api/routes/tokens.ts +0 -80
- package/src/api/schemas/auth.ts +0 -15
- package/src/api/schemas/index.ts +0 -51
- package/src/api/schemas/tokens.ts +0 -24
- package/src/auth/index.ts +0 -28
- package/src/cli/commands.ts +0 -217
- package/src/cli/index.ts +0 -21
- package/src/cli/mcp.ts +0 -210
- package/src/cli/tests/cli.test.ts +0 -40
- package/src/cli/tests/create.test.ts +0 -87
- package/src/client/FlareAdminRouter.tsx +0 -47
- package/src/client/app.tsx +0 -175
- package/src/client/components/app-sidebar.tsx +0 -227
- package/src/client/components/collection-modal.tsx +0 -215
- package/src/client/components/content-list.tsx +0 -247
- package/src/client/components/dynamic-form.tsx +0 -190
- package/src/client/components/field-modal.tsx +0 -221
- package/src/client/components/settings/api-token-section.tsx +0 -400
- package/src/client/components/settings/general-section.tsx +0 -224
- package/src/client/components/settings/security-section.tsx +0 -154
- package/src/client/components/settings/seo-section.tsx +0 -200
- package/src/client/components/settings/signup-section.tsx +0 -257
- package/src/client/components/ui/accordion.tsx +0 -78
- package/src/client/components/ui/avatar.tsx +0 -107
- package/src/client/components/ui/badge.tsx +0 -52
- package/src/client/components/ui/button.tsx +0 -60
- package/src/client/components/ui/card.tsx +0 -103
- package/src/client/components/ui/checkbox.tsx +0 -27
- package/src/client/components/ui/collapsible.tsx +0 -19
- package/src/client/components/ui/dialog.tsx +0 -162
- package/src/client/components/ui/icon-picker.tsx +0 -485
- package/src/client/components/ui/icons-data.ts +0 -8476
- package/src/client/components/ui/input.tsx +0 -20
- package/src/client/components/ui/label.tsx +0 -20
- package/src/client/components/ui/popover.tsx +0 -91
- package/src/client/components/ui/select.tsx +0 -204
- package/src/client/components/ui/separator.tsx +0 -23
- package/src/client/components/ui/sheet.tsx +0 -141
- package/src/client/components/ui/sidebar.tsx +0 -722
- package/src/client/components/ui/skeleton.tsx +0 -13
- package/src/client/components/ui/sonner.tsx +0 -47
- package/src/client/components/ui/switch.tsx +0 -30
- package/src/client/components/ui/table.tsx +0 -116
- package/src/client/components/ui/tabs.tsx +0 -80
- package/src/client/components/ui/textarea.tsx +0 -18
- package/src/client/components/ui/tooltip.tsx +0 -68
- package/src/client/hooks/use-mobile.ts +0 -19
- package/src/client/index.css +0 -149
- package/src/client/index.ts +0 -7
- package/src/client/layouts/admin-layout.tsx +0 -93
- package/src/client/layouts/settings-layout.tsx +0 -104
- package/src/client/lib/api.ts +0 -72
- package/src/client/lib/utils.ts +0 -6
- package/src/client/main.tsx +0 -10
- package/src/client/pages/collection-detail.tsx +0 -634
- package/src/client/pages/collections.tsx +0 -180
- package/src/client/pages/dashboard.tsx +0 -133
- package/src/client/pages/device.tsx +0 -66
- package/src/client/pages/document-detail-page.tsx +0 -139
- package/src/client/pages/documents-page.tsx +0 -103
- package/src/client/pages/login.tsx +0 -345
- package/src/client/pages/settings.tsx +0 -65
- package/src/client/pages/setup.tsx +0 -129
- package/src/client/pages/signup.tsx +0 -188
- package/src/client/store/auth.ts +0 -30
- package/src/client/store/collections.ts +0 -13
- package/src/client/store/config.ts +0 -12
- package/src/client/store/fetcher.ts +0 -30
- package/src/client/store/router.ts +0 -95
- package/src/client/store/schema.ts +0 -39
- package/src/client/store/settings.ts +0 -31
- package/src/client/types.ts +0 -34
- package/src/db/dynamic.ts +0 -70
- package/src/db/index.ts +0 -16
- package/src/db/migrations/001_initial_schema.ts +0 -57
- package/src/db/migrations/002_auth_tables.ts +0 -84
- package/src/db/migrator.ts +0 -61
- package/src/db/schema.ts +0 -142
- package/src/index.ts +0 -12
- package/src/server/index.ts +0 -66
- package/src/types.ts +0 -20
- package/tests/css.test.ts +0 -21
- package/tests/modular.test.ts +0 -29
- package/tsconfig.json +0 -10
- /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
|
-
}
|