@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 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.0",
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: 'Users' },
8
- { id: 'settings', index: '03', label: 'Settings' }
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@v0.1.0</small>
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}>[ logout ]</button>
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>Overview</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}>Loading dashboard...</p> : null}
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>Total users</span>
39
+ <span>Utilisateurs</span>
40
40
  <strong>{summary.userCount}</strong>
41
41
  </article>
42
42
  <article className={styles.wideCard}>
43
- <span>Role breakdown</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}>
@@ -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}>v0.1.0</span>
33
+ <span className={styles.tagMuted}>v1.0.0</span>
34
34
  </div>
35
35
  <h1 id="login-title">
36
- <span className={styles.prompt}>&gt;</span> sign_in
36
+ <span className={styles.prompt}>&gt;</span> connexion
37
37
  <span className={styles.cursor} aria-hidden="true" />
38
38
  </h1>
39
39
  <p className={styles.copy}>
40
- // authenticate with the seeded owner account to inspect the generic SaaS API screens
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
- password
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 ? 'authenticating...' : '[ sign in ]'}</button>
52
+ <button type="submit" disabled={loading}>{loading ? 'connexion...' : '[ se connecter ]'}</button>
53
53
  </form>
54
54
  </section>
55
55
  </main>
@@ -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 ? 'Not authorized to manage settings.' : apiError.message);
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} saved.`);
45
+ setSuccess(`${updated.key} enregistre.`);
46
46
  } catch (apiError) {
47
- setError(apiError.status === 403 ? 'Not authorized to update settings.' : apiError.message);
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>Settings</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}>No settings yet.</p> : null}
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)}>Save</button>
72
+ <button type="button" onClick={() => saveSetting(key)}>Enregistrer</button>
73
73
  </div>
74
74
  ))}
75
75
  </div>
@@ -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
- setError(apiError.status === 403 ? 'Not authorized to manage users.' : apiError.message);
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('User created.');
45
+ setSuccess('Utilisateur cree.');
39
46
  await loadUsers();
40
47
  } catch (apiError) {
41
- setError(apiError.status === 403 ? 'Not authorized to create users.' : apiError.message);
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>Users</h1>
56
+ <h1>Utilisateurs</h1>
50
57
  </header>
51
58
  <Notice tone="error">{error}</Notice>
52
59
  <Notice tone="success">{success}</Notice>
53
- <form className={styles.inlineForm} onSubmit={createUser}>
54
- <input
55
- aria-label="Email"
56
- placeholder="email@example.test"
57
- type="email"
58
- value={form.email}
59
- onChange={(event) => setForm({ ...form, email: event.target.value })}
60
- required
61
- />
62
- <select
63
- aria-label="Role"
64
- value={form.role}
65
- onChange={(event) => setForm({ ...form, role: event.target.value })}
66
- >
67
- <option value="member">member</option>
68
- <option value="admin">admin</option>
69
- <option value="owner">owner</option>
70
- </select>
71
- <input
72
- aria-label="Password"
73
- value={form.password}
74
- onChange={(event) => setForm({ ...form, password: event.target.value })}
75
- required
76
- />
77
- <button type="submit">Create user</button>
78
- </form>
79
- {loading ? <p className={styles.muted}>Loading users...</p> : null}
80
- <div className={styles.tableWrap}>
81
- <table>
82
- <thead>
83
- <tr>
84
- <th>Email</th>
85
- <th>Role</th>
86
- </tr>
87
- </thead>
88
- <tbody>
89
- {users.map((user) => (
90
- <tr key={user.id}>
91
- <td>{user.email}</td>
92
- <td>{user.role || 'unknown'}</td>
93
- </tr>
94
- ))}
95
- </tbody>
96
- </table>
97
- </div>
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
  }