@astratra/saas-kit-ui 0.1.0 → 1.0.1
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/README.md +23 -0
- package/package.json +4 -1
- package/src/components/Layout.jsx +5 -5
- package/src/screens/Dashboard.jsx +4 -4
- package/src/screens/Login.jsx +5 -5
- package/src/screens/Settings.jsx +6 -6
- package/src/screens/Users.jsx +60 -49
package/README.md
CHANGED
|
@@ -44,3 +44,26 @@ VITE_API_URL=http://localhost:4000 npm run dev
|
|
|
44
44
|
|
|
45
45
|
Le package ne contient pas de serveur Express et ne fixe aucun domaine metier.
|
|
46
46
|
Le backend reste fourni par `@astratra/saas-kit` ou par ton application.
|
|
47
|
+
|
|
48
|
+
## `AuthProvider`/`useAuth` vs `@astratra/react`
|
|
49
|
+
|
|
50
|
+
Deux packages Astratra exposent des primitives de session React, avec des
|
|
51
|
+
choix différents et volontaires — ce n'est pas un doublon accidentel :
|
|
52
|
+
|
|
53
|
+
- **`AuthProvider`/`useAuth` (ce package)** : garde le JWT en mémoire JS et
|
|
54
|
+
l'attache en `Authorization: Bearer` via `apiFetch`. Couplé au dashboard
|
|
55
|
+
complet `AstratraDashboardApp` (login/users/settings/overview) — conçu
|
|
56
|
+
pour démarrer vite avec un backend `@astratra/saas-kit` standard, pas pour
|
|
57
|
+
être réutilisé isolément dans une UI déjà existante.
|
|
58
|
+
- **`SessionProvider`/`useSession` (`@astratra/react`)** : ne stocke aucun
|
|
59
|
+
token côté client — la session vit dans un cookie `HttpOnly` géré par le
|
|
60
|
+
backend, l'app injecte juste `getSession`/`signIn`/`signOut`. Pas de
|
|
61
|
+
dashboard imposé, pas de routes fixes ; à choisir quand tu construis ta
|
|
62
|
+
propre UI par-dessus `@astratra/saas-kit` (avec les cookies de session
|
|
63
|
+
activés côté `@astratra/security`) plutôt que d'utiliser
|
|
64
|
+
`AstratraDashboardApp` tel quel.
|
|
65
|
+
|
|
66
|
+
En résumé : utilise ce package si `AstratraDashboardApp` te convient tel
|
|
67
|
+
quel ; utilise `@astratra/react` si tu construis ta propre interface et que
|
|
68
|
+
ton backend utilise les sessions cookie `HttpOnly` plutôt qu'un token Bearer
|
|
69
|
+
géré côté client.
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astratra/saas-kit-ui",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Reusable React dashboard shell for Astratra SaaS applications.",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=20"
|
|
7
|
+
},
|
|
5
8
|
"type": "module",
|
|
6
9
|
"main": "src/index.jsx",
|
|
7
10
|
"module": "src/index.jsx",
|
|
@@ -4,8 +4,8 @@ import styles from './Layout.module.css';
|
|
|
4
4
|
|
|
5
5
|
const NAV_ITEMS = [
|
|
6
6
|
{ id: 'dashboard', index: '01', label: 'Dashboard' },
|
|
7
|
-
{ id: 'users', index: '02', label: '
|
|
8
|
-
{ id: 'settings', index: '03', label: '
|
|
7
|
+
{ id: 'users', index: '02', label: 'Utilisateurs' },
|
|
8
|
+
{ id: 'settings', index: '03', label: 'Parametres' }
|
|
9
9
|
];
|
|
10
10
|
|
|
11
11
|
export default function Layout({ activeView, children, onNavigate }) {
|
|
@@ -18,7 +18,7 @@ export default function Layout({ activeView, children, onNavigate }) {
|
|
|
18
18
|
<img src={logoUrl} alt="Astratra" className={styles.brandMark} />
|
|
19
19
|
<span>
|
|
20
20
|
<strong>Astratra</strong>
|
|
21
|
-
<small>saas_starter@
|
|
21
|
+
<small>saas_starter@v1.0.0</small>
|
|
22
22
|
</span>
|
|
23
23
|
</div>
|
|
24
24
|
|
|
@@ -43,8 +43,8 @@ export default function Layout({ activeView, children, onNavigate }) {
|
|
|
43
43
|
|
|
44
44
|
<div className={styles.account}>
|
|
45
45
|
<span>{user?.email}</span>
|
|
46
|
-
<small>role: {user?.role}</small>
|
|
47
|
-
<button type="button" onClick={logout}>[
|
|
46
|
+
<small>role : {user?.role}</small>
|
|
47
|
+
<button type="button" onClick={logout}>[ deconnexion ]</button>
|
|
48
48
|
</div>
|
|
49
49
|
</aside>
|
|
50
50
|
<main className={styles.content}>{children}</main>
|
|
@@ -28,19 +28,19 @@ export default function Dashboard() {
|
|
|
28
28
|
return (
|
|
29
29
|
<section className={styles.screen}>
|
|
30
30
|
<header className={styles.header}>
|
|
31
|
-
<p>
|
|
31
|
+
<p>Vue d'ensemble</p>
|
|
32
32
|
<h1>Dashboard</h1>
|
|
33
33
|
</header>
|
|
34
34
|
<Notice tone="error">{error}</Notice>
|
|
35
|
-
{!summary && !error ? <p className={styles.muted}>
|
|
35
|
+
{!summary && !error ? <p className={styles.muted}>Chargement du dashboard...</p> : null}
|
|
36
36
|
{summary ? (
|
|
37
37
|
<div className={styles.grid}>
|
|
38
38
|
<article className={styles.statCard}>
|
|
39
|
-
<span>
|
|
39
|
+
<span>Utilisateurs</span>
|
|
40
40
|
<strong>{summary.userCount}</strong>
|
|
41
41
|
</article>
|
|
42
42
|
<article className={styles.wideCard}>
|
|
43
|
-
<span>
|
|
43
|
+
<span>Repartition des roles</span>
|
|
44
44
|
<div className={styles.bars}>
|
|
45
45
|
{roles.map(([role, count]) => (
|
|
46
46
|
<div key={role} className={styles.barRow}>
|
package/src/screens/Login.jsx
CHANGED
|
@@ -30,14 +30,14 @@ export default function Login({ onLoggedIn }) {
|
|
|
30
30
|
<div className={styles.frame} aria-hidden="true" />
|
|
31
31
|
<div className={styles.identity}>
|
|
32
32
|
<span className={styles.tag}>astratra/saas-kit</span>
|
|
33
|
-
<span className={styles.tagMuted}>
|
|
33
|
+
<span className={styles.tagMuted}>v1.0.0</span>
|
|
34
34
|
</div>
|
|
35
35
|
<h1 id="login-title">
|
|
36
|
-
<span className={styles.prompt}>></span>
|
|
36
|
+
<span className={styles.prompt}>></span> connexion
|
|
37
37
|
<span className={styles.cursor} aria-hidden="true" />
|
|
38
38
|
</h1>
|
|
39
39
|
<p className={styles.copy}>
|
|
40
|
-
//
|
|
40
|
+
// connectez-vous avec le compte owner de demo pour explorer l'API SaaS generique
|
|
41
41
|
</p>
|
|
42
42
|
<Notice tone="error">{error}</Notice>
|
|
43
43
|
<form className={styles.form} onSubmit={handleSubmit}>
|
|
@@ -46,10 +46,10 @@ export default function Login({ onLoggedIn }) {
|
|
|
46
46
|
<input value={email} onChange={(event) => setEmail(event.target.value)} type="email" required />
|
|
47
47
|
</label>
|
|
48
48
|
<label>
|
|
49
|
-
|
|
49
|
+
mot de passe
|
|
50
50
|
<input value={password} onChange={(event) => setPassword(event.target.value)} type="password" required />
|
|
51
51
|
</label>
|
|
52
|
-
<button type="submit" disabled={loading}>{loading ? '
|
|
52
|
+
<button type="submit" disabled={loading}>{loading ? 'connexion...' : '[ se connecter ]'}</button>
|
|
53
53
|
</form>
|
|
54
54
|
</section>
|
|
55
55
|
</main>
|
package/src/screens/Settings.jsx
CHANGED
|
@@ -17,7 +17,7 @@ export default function Settings() {
|
|
|
17
17
|
setSettings(data || {});
|
|
18
18
|
setDrafts(Object.fromEntries(Object.entries(data || {}).map(([key, value]) => [key, JSON.stringify(value)])));
|
|
19
19
|
} catch (apiError) {
|
|
20
|
-
setError(apiError.status === 403 ? '
|
|
20
|
+
setError(apiError.status === 403 ? 'Vous n etes pas autorise a gerer les parametres.' : apiError.message);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -42,9 +42,9 @@ export default function Settings() {
|
|
|
42
42
|
});
|
|
43
43
|
setSettings({ ...settings, [updated.key]: updated.value });
|
|
44
44
|
setDrafts({ ...drafts, [updated.key]: JSON.stringify(updated.value) });
|
|
45
|
-
setSuccess(`${updated.key}
|
|
45
|
+
setSuccess(`${updated.key} enregistre.`);
|
|
46
46
|
} catch (apiError) {
|
|
47
|
-
setError(apiError.status === 403 ? '
|
|
47
|
+
setError(apiError.status === 403 ? 'Vous n etes pas autorise a modifier les parametres.' : apiError.message);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -54,11 +54,11 @@ export default function Settings() {
|
|
|
54
54
|
<section className={styles.screen}>
|
|
55
55
|
<header className={styles.header}>
|
|
56
56
|
<p>Configuration</p>
|
|
57
|
-
<h1>
|
|
57
|
+
<h1>Parametres</h1>
|
|
58
58
|
</header>
|
|
59
59
|
<Notice tone="error">{error}</Notice>
|
|
60
60
|
<Notice tone="success">{success}</Notice>
|
|
61
|
-
{rows.length === 0 && !error ? <p className={styles.muted}>
|
|
61
|
+
{rows.length === 0 && !error ? <p className={styles.muted}>Aucun parametre pour le moment.</p> : null}
|
|
62
62
|
<div className={styles.settingsList}>
|
|
63
63
|
{rows.map(([key]) => (
|
|
64
64
|
<div className={styles.settingRow} key={key}>
|
|
@@ -69,7 +69,7 @@ export default function Settings() {
|
|
|
69
69
|
onChange={(event) => setDrafts({ ...drafts, [key]: event.target.value })}
|
|
70
70
|
/>
|
|
71
71
|
</label>
|
|
72
|
-
<button type="button" onClick={() => saveSetting(key)}>
|
|
72
|
+
<button type="button" onClick={() => saveSetting(key)}>Enregistrer</button>
|
|
73
73
|
</div>
|
|
74
74
|
))}
|
|
75
75
|
</div>
|
package/src/screens/Users.jsx
CHANGED
|
@@ -10,6 +10,7 @@ export default function Users() {
|
|
|
10
10
|
const [success, setSuccess] = useState('');
|
|
11
11
|
const [form, setForm] = useState({ email: '', role: 'member', password: 'password' });
|
|
12
12
|
const [loading, setLoading] = useState(true);
|
|
13
|
+
const [authorized, setAuthorized] = useState(true);
|
|
13
14
|
|
|
14
15
|
async function loadUsers() {
|
|
15
16
|
setLoading(true);
|
|
@@ -17,8 +18,14 @@ export default function Users() {
|
|
|
17
18
|
try {
|
|
18
19
|
const data = await request('/users?limit=50&offset=0');
|
|
19
20
|
setUsers(data.items || []);
|
|
21
|
+
setAuthorized(true);
|
|
20
22
|
} catch (apiError) {
|
|
21
|
-
|
|
23
|
+
if (apiError.status === 403) {
|
|
24
|
+
setAuthorized(false);
|
|
25
|
+
setError('Vous n etes pas autorise a gerer les utilisateurs.');
|
|
26
|
+
} else {
|
|
27
|
+
setError(apiError.message);
|
|
28
|
+
}
|
|
22
29
|
} finally {
|
|
23
30
|
setLoading(false);
|
|
24
31
|
}
|
|
@@ -35,10 +42,10 @@ export default function Users() {
|
|
|
35
42
|
try {
|
|
36
43
|
await request('/users', { method: 'POST', body: form });
|
|
37
44
|
setForm({ email: '', role: 'member', password: 'password' });
|
|
38
|
-
setSuccess('
|
|
45
|
+
setSuccess('Utilisateur cree.');
|
|
39
46
|
await loadUsers();
|
|
40
47
|
} catch (apiError) {
|
|
41
|
-
setError(apiError.status === 403 ? '
|
|
48
|
+
setError(apiError.status === 403 ? 'Vous n etes pas autorise a creer des utilisateurs.' : apiError.message);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -46,55 +53,59 @@ export default function Users() {
|
|
|
46
53
|
<section className={styles.screen}>
|
|
47
54
|
<header className={styles.header}>
|
|
48
55
|
<p>Administration</p>
|
|
49
|
-
<h1>
|
|
56
|
+
<h1>Utilisateurs</h1>
|
|
50
57
|
</header>
|
|
51
58
|
<Notice tone="error">{error}</Notice>
|
|
52
59
|
<Notice tone="success">{success}</Notice>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
60
|
+
{authorized ? (
|
|
61
|
+
<>
|
|
62
|
+
<form className={styles.inlineForm} onSubmit={createUser}>
|
|
63
|
+
<input
|
|
64
|
+
aria-label="Email"
|
|
65
|
+
placeholder="email@example.test"
|
|
66
|
+
type="email"
|
|
67
|
+
value={form.email}
|
|
68
|
+
onChange={(event) => setForm({ ...form, email: event.target.value })}
|
|
69
|
+
required
|
|
70
|
+
/>
|
|
71
|
+
<select
|
|
72
|
+
aria-label="Role"
|
|
73
|
+
value={form.role}
|
|
74
|
+
onChange={(event) => setForm({ ...form, role: event.target.value })}
|
|
75
|
+
>
|
|
76
|
+
<option value="member">member</option>
|
|
77
|
+
<option value="admin">admin</option>
|
|
78
|
+
<option value="owner">owner</option>
|
|
79
|
+
</select>
|
|
80
|
+
<input
|
|
81
|
+
aria-label="Mot de passe"
|
|
82
|
+
value={form.password}
|
|
83
|
+
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
|
84
|
+
required
|
|
85
|
+
/>
|
|
86
|
+
<button type="submit">Creer un utilisateur</button>
|
|
87
|
+
</form>
|
|
88
|
+
{loading ? <p className={styles.muted}>Chargement des utilisateurs...</p> : null}
|
|
89
|
+
<div className={styles.tableWrap}>
|
|
90
|
+
<table>
|
|
91
|
+
<thead>
|
|
92
|
+
<tr>
|
|
93
|
+
<th>Email</th>
|
|
94
|
+
<th>Role</th>
|
|
95
|
+
</tr>
|
|
96
|
+
</thead>
|
|
97
|
+
<tbody>
|
|
98
|
+
{users.map((user) => (
|
|
99
|
+
<tr key={user.id}>
|
|
100
|
+
<td>{user.email}</td>
|
|
101
|
+
<td>{user.role || 'inconnu'}</td>
|
|
102
|
+
</tr>
|
|
103
|
+
))}
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
</div>
|
|
107
|
+
</>
|
|
108
|
+
) : null}
|
|
98
109
|
</section>
|
|
99
110
|
);
|
|
100
111
|
}
|