sprygen 1.0.0

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 (56) hide show
  1. package/README.md +80 -0
  2. package/dist/cli.js +55 -0
  3. package/package.json +53 -0
  4. package/templates/auth/AuthController.java.ejs +40 -0
  5. package/templates/auth/JwtAuthFilter.java.ejs +62 -0
  6. package/templates/auth/JwtService.java.ejs +81 -0
  7. package/templates/auth/SecurityConfig.java.ejs +65 -0
  8. package/templates/auth/UserDetailsServiceImpl.java.ejs +24 -0
  9. package/templates/entity/Entity.java.ejs +40 -0
  10. package/templates/entity/EntityController.java.ejs +92 -0
  11. package/templates/entity/EntityControllerTest.java.ejs +24 -0
  12. package/templates/entity/EntityDto.java.ejs +32 -0
  13. package/templates/entity/EntityRepository.java.ejs +9 -0
  14. package/templates/entity/EntityService.java.ejs +32 -0
  15. package/templates/project/java/config/CorsConfig.java.ejs +24 -0
  16. package/templates/project/java/config/SecurityConfig.java.ejs +76 -0
  17. package/templates/project/java/config/SecurityConfigSession.java.ejs +73 -0
  18. package/templates/project/java/config/SwaggerConfig.java.ejs +31 -0
  19. package/templates/project/java/controller/AdminController.java.ejs +82 -0
  20. package/templates/project/java/controller/AuthController.java.ejs +86 -0
  21. package/templates/project/java/controller/HomeController.java.ejs +63 -0
  22. package/templates/project/java/controller/ProfileController.java.ejs +65 -0
  23. package/templates/project/java/controller/UserController.java.ejs +35 -0
  24. package/templates/project/java/dto/AuthRequest.java.ejs +15 -0
  25. package/templates/project/java/dto/AuthResponse.java.ejs +18 -0
  26. package/templates/project/java/dto/ProfileUpdateRequest.java.ejs +20 -0
  27. package/templates/project/java/dto/RegisterRequest.java.ejs +30 -0
  28. package/templates/project/java/dto/UserDto.java.ejs +17 -0
  29. package/templates/project/java/entity/Role.java.ejs +6 -0
  30. package/templates/project/java/entity/User.java.ejs +97 -0
  31. package/templates/project/java/repository/UserRepository.java.ejs +11 -0
  32. package/templates/project/java/security/JwtAuthFilter.java.ejs +62 -0
  33. package/templates/project/java/security/UserDetailsServiceImpl.java.ejs +21 -0
  34. package/templates/project/java/service/JwtService.java.ejs +81 -0
  35. package/templates/project/java/service/UserService.java.ejs +32 -0
  36. package/templates/project/resources/application.yml.ejs +50 -0
  37. package/templates/project/resources/logback-spring.xml.ejs +41 -0
  38. package/templates/project/static/admin.html.ejs +163 -0
  39. package/templates/project/static/assets/app.js.ejs +340 -0
  40. package/templates/project/static/assets/style.css +533 -0
  41. package/templates/project/static/css/style.css +595 -0
  42. package/templates/project/static/dashboard.html.ejs +119 -0
  43. package/templates/project/static/index.html.ejs +96 -0
  44. package/templates/project/static/js/api.js +30 -0
  45. package/templates/project/static/js/auth.js +44 -0
  46. package/templates/project/static/js/nav.js.ejs +82 -0
  47. package/templates/project/static/js/ui.js +57 -0
  48. package/templates/project/static/login.html.ejs +71 -0
  49. package/templates/project/static/profile.html.ejs +163 -0
  50. package/templates/project/static/register.html.ejs +82 -0
  51. package/templates/project/thymeleaf/admin/users.html.ejs +111 -0
  52. package/templates/project/thymeleaf/dashboard.html.ejs +109 -0
  53. package/templates/project/thymeleaf/layout.html.ejs +75 -0
  54. package/templates/project/thymeleaf/login.html.ejs +56 -0
  55. package/templates/project/thymeleaf/profile.html.ejs +133 -0
  56. package/templates/project/thymeleaf/register.html.ejs +56 -0
@@ -0,0 +1,340 @@
1
+ /* ================================================================
2
+ <%= projectName %> β€” Frontend SPA
3
+ JWT-based client router + API client
4
+ ================================================================ */
5
+
6
+ const API = ''; // same origin β€” Spring Boot serves this file
7
+
8
+ // ----------------------------------------------------------------
9
+ // Storage helpers
10
+ // ----------------------------------------------------------------
11
+ const Auth = {
12
+ save(data) {
13
+ localStorage.setItem('_token', data.token);
14
+ localStorage.setItem('_user', JSON.stringify({
15
+ email: data.email,
16
+ firstName: data.firstName,
17
+ lastName: data.lastName,
18
+ role: data.role,
19
+ }));
20
+ },
21
+ clear() {
22
+ localStorage.removeItem('_token');
23
+ localStorage.removeItem('_user');
24
+ },
25
+ token() { return localStorage.getItem('_token'); },
26
+ user() { try { return JSON.parse(localStorage.getItem('_user') || 'null'); } catch { return null; } },
27
+ isAdmin() { const u = Auth.user(); return u && u.role === 'ROLE_ADMIN'; },
28
+ loggedIn() { return !!Auth.token(); },
29
+ };
30
+
31
+ // ----------------------------------------------------------------
32
+ // API client
33
+ // ----------------------------------------------------------------
34
+ async function api(method, path, body) {
35
+ const headers = { 'Content-Type': 'application/json' };
36
+ if (Auth.token()) headers['Authorization'] = 'Bearer ' + Auth.token();
37
+ const res = await fetch(API + path, {
38
+ method,
39
+ headers,
40
+ body: body ? JSON.stringify(body) : undefined,
41
+ });
42
+ if (res.status === 204) return {};
43
+ const json = await res.json().catch(() => ({}));
44
+ if (!res.ok) throw new Error(json.message || json.error || 'Request failed (' + res.status + ')');
45
+ return json;
46
+ }
47
+
48
+ // ----------------------------------------------------------------
49
+ // Toast
50
+ // ----------------------------------------------------------------
51
+ let toastTimer;
52
+ function showToast(msg, type = 'success') {
53
+ const el = document.getElementById('toast');
54
+ el.textContent = msg;
55
+ el.className = 'toast toast-' + type + ' show';
56
+ clearTimeout(toastTimer);
57
+ toastTimer = setTimeout(() => el.classList.remove('show'), 3200);
58
+ }
59
+
60
+ // ----------------------------------------------------------------
61
+ // Alert helpers
62
+ // ----------------------------------------------------------------
63
+ function showAlert(id, msg, type = 'error') {
64
+ const el = document.getElementById(id);
65
+ if (!el) return;
66
+ el.textContent = msg;
67
+ el.className = 'alert alert-' + type + ' visible';
68
+ }
69
+ function clearAlert(id) {
70
+ const el = document.getElementById(id);
71
+ if (el) el.className = 'alert';
72
+ }
73
+
74
+ // ----------------------------------------------------------------
75
+ // Page / view router
76
+ // ----------------------------------------------------------------
77
+ const pages = ['landing', 'login', 'register', 'app'];
78
+
79
+ function showPage(name) {
80
+ pages.forEach(p => {
81
+ const el = document.getElementById('page-' + p);
82
+ if (el) el.classList.toggle('hidden', p !== name);
83
+ });
84
+ window.scrollTo(0, 0);
85
+ }
86
+
87
+ function navigate(view) {
88
+ ['dashboard', 'profile', 'admin'].forEach(v => {
89
+ const el = document.getElementById('view-' + v);
90
+ if (el) el.classList.toggle('hidden', v !== view);
91
+ const navEl = document.getElementById('nav-' + v);
92
+ if (navEl) navEl.classList.toggle('active', v === view);
93
+ });
94
+
95
+ if (view === 'dashboard') loadDashboard();
96
+ if (view === 'profile') loadProfile();
97
+ if (view === 'admin') loadAdminUsers();
98
+ }
99
+
100
+ // ----------------------------------------------------------------
101
+ // Auth flow
102
+ // ----------------------------------------------------------------
103
+ document.getElementById('login-form').addEventListener('submit', async e => {
104
+ e.preventDefault();
105
+ clearAlert('login-alert');
106
+ const btn = document.getElementById('login-btn');
107
+ btn.disabled = true;
108
+ btn.textContent = 'Signing in…';
109
+ try {
110
+ const data = await api('POST', '/api/v1/auth/login', {
111
+ email: document.getElementById('login-email').value,
112
+ password: document.getElementById('login-password').value,
113
+ });
114
+ Auth.save(data);
115
+ enterApp();
116
+ } catch (err) {
117
+ showAlert('login-alert', err.message);
118
+ } finally {
119
+ btn.disabled = false;
120
+ btn.textContent = 'Sign In';
121
+ }
122
+ });
123
+
124
+ document.getElementById('register-form').addEventListener('submit', async e => {
125
+ e.preventDefault();
126
+ clearAlert('register-alert');
127
+ const btn = document.getElementById('register-btn');
128
+ btn.disabled = true;
129
+ btn.textContent = 'Creating account…';
130
+ try {
131
+ const data = await api('POST', '/api/v1/auth/register', {
132
+ firstName: document.getElementById('reg-first').value,
133
+ lastName: document.getElementById('reg-last').value,
134
+ email: document.getElementById('reg-email').value,
135
+ password: document.getElementById('reg-password').value,
136
+ });
137
+ Auth.save(data);
138
+ enterApp();
139
+ showToast('Welcome to <%= projectName %>! πŸŽ‰');
140
+ } catch (err) {
141
+ showAlert('register-alert', err.message);
142
+ } finally {
143
+ btn.disabled = false;
144
+ btn.textContent = 'Create Account';
145
+ }
146
+ });
147
+
148
+ function enterApp() {
149
+ const user = Auth.user();
150
+ if (!user) return;
151
+
152
+ // Sidebar user widget
153
+ const initials = (user.firstName[0] || '') + (user.lastName[0] || '');
154
+ document.getElementById('sidebar-avatar').textContent = initials.toUpperCase();
155
+ document.getElementById('sidebar-name').textContent = user.firstName + ' ' + user.lastName;
156
+ document.getElementById('sidebar-role').textContent = Auth.isAdmin() ? 'πŸ‘‘ Admin' : 'πŸ™‚ User';
157
+
158
+ // Show/hide admin links
159
+ document.querySelectorAll('.admin-only').forEach(el => {
160
+ el.classList.toggle('hidden', !Auth.isAdmin());
161
+ });
162
+
163
+ showPage('app');
164
+ navigate('dashboard');
165
+ }
166
+
167
+ function logout() {
168
+ Auth.clear();
169
+ showPage('landing');
170
+ }
171
+
172
+ // ----------------------------------------------------------------
173
+ // Dashboard
174
+ // ----------------------------------------------------------------
175
+ async function loadDashboard() {
176
+ const user = Auth.user();
177
+ document.getElementById('dash-name').textContent = user?.firstName || '';
178
+ document.getElementById('topbar-greeting').textContent =
179
+ new Date().toLocaleDateString('en-US', { weekday:'long', month:'long', day:'numeric' });
180
+ document.getElementById('stat-role').textContent = Auth.isAdmin() ? 'Admin' : 'User';
181
+
182
+ if (Auth.isAdmin()) {
183
+ try {
184
+ const stats = await api('GET', '/api/v1/admin/stats');
185
+ document.getElementById('stat-total-users').textContent = stats.totalUsers ?? 'β€”';
186
+ document.getElementById('stat-admins').textContent = stats.admins ?? 'β€”';
187
+ } catch { /* silently ignore */ }
188
+ }
189
+ }
190
+
191
+ // ----------------------------------------------------------------
192
+ // Profile
193
+ // ----------------------------------------------------------------
194
+ async function loadProfile() {
195
+ try {
196
+ const user = await api('GET', '/api/v1/profile');
197
+ document.getElementById('profile-hero-name').textContent = user.firstName + ' ' + user.lastName;
198
+ document.getElementById('profile-hero-email').textContent = user.email;
199
+ const initials = (user.firstName?.[0] || '') + (user.lastName?.[0] || '');
200
+ document.getElementById('profile-avatar-lg').textContent = initials.toUpperCase();
201
+
202
+ const isAdmin = user.roles?.includes('ROLE_ADMIN');
203
+ document.getElementById('profile-hero-badge').innerHTML =
204
+ isAdmin ? '<span class="badge badge-admin">πŸ‘‘ Admin</span>' : '<span class="badge badge-user">User</span>';
205
+
206
+ document.getElementById('pf-first').value = user.firstName || '';
207
+ document.getElementById('pf-last').value = user.lastName || '';
208
+ document.getElementById('pf-bio').value = user.bio || '';
209
+ document.getElementById('pf-avatar').value = user.avatarUrl || '';
210
+ } catch (err) {
211
+ showToast('Failed to load profile: ' + err.message, 'error');
212
+ }
213
+ }
214
+
215
+ document.getElementById('profile-form').addEventListener('submit', async e => {
216
+ e.preventDefault();
217
+ clearAlert('profile-alert');
218
+ try {
219
+ await api('PUT', '/api/v1/profile', {
220
+ firstName: document.getElementById('pf-first').value,
221
+ lastName: document.getElementById('pf-last').value,
222
+ bio: document.getElementById('pf-bio').value,
223
+ avatarUrl: document.getElementById('pf-avatar').value,
224
+ });
225
+
226
+ // Update sidebar
227
+ const user = Auth.user();
228
+ if (user) {
229
+ user.firstName = document.getElementById('pf-first').value;
230
+ user.lastName = document.getElementById('pf-last').value;
231
+ localStorage.setItem('_user', JSON.stringify(user));
232
+ document.getElementById('sidebar-name').textContent = user.firstName + ' ' + user.lastName;
233
+ document.getElementById('sidebar-avatar').textContent =
234
+ (user.firstName[0] + user.lastName[0]).toUpperCase();
235
+ }
236
+
237
+ showToast('Profile updated successfully βœ“');
238
+ loadProfile();
239
+ } catch (err) {
240
+ showAlert('profile-alert', err.message, 'error');
241
+ document.getElementById('profile-alert').classList.add('visible');
242
+ }
243
+ });
244
+
245
+ document.getElementById('password-form').addEventListener('submit', async e => {
246
+ e.preventDefault();
247
+ clearAlert('pw-alert');
248
+ const newPw = document.getElementById('pw-new').value;
249
+ const confirm = document.getElementById('pw-confirm').value;
250
+ if (newPw !== confirm) {
251
+ showAlert('pw-alert', 'New passwords do not match.', 'error');
252
+ document.getElementById('pw-alert').classList.add('visible');
253
+ return;
254
+ }
255
+ try {
256
+ await api('PUT', '/api/v1/profile/password', {
257
+ currentPassword: document.getElementById('pw-current').value,
258
+ newPassword: newPw,
259
+ });
260
+ document.getElementById('password-form').reset();
261
+ showToast('Password changed successfully βœ“');
262
+ } catch (err) {
263
+ showAlert('pw-alert', err.message, 'error');
264
+ document.getElementById('pw-alert').classList.add('visible');
265
+ }
266
+ });
267
+
268
+ // ----------------------------------------------------------------
269
+ // Admin panel
270
+ // ----------------------------------------------------------------
271
+ async function loadAdminUsers() {
272
+ const tbody = document.getElementById('admin-users-body');
273
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:32px;color:var(--text-muted);">Loading users…</td></tr>';
274
+ try {
275
+ const users = await api('GET', '/api/v1/admin/users');
276
+ document.getElementById('admin-user-count').textContent = users.length + ' users';
277
+
278
+ if (!users.length) {
279
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:32px;color:var(--text-muted);">No users found.</td></tr>';
280
+ return;
281
+ }
282
+
283
+ tbody.innerHTML = users.map(u => {
284
+ const isAdmin = u.roles?.includes('ROLE_ADMIN');
285
+ const joined = u.createdAt ? new Date(u.createdAt).toLocaleDateString() : 'β€”';
286
+ const roleBadge = isAdmin
287
+ ? '<span class="badge badge-admin">πŸ‘‘ Admin</span>'
288
+ : '<span class="badge badge-user">User</span>';
289
+ const toggleLabel = isAdmin ? 'Make User' : 'Make Admin';
290
+ const newRole = isAdmin ? 'ROLE_USER' : 'ROLE_ADMIN';
291
+ return `<tr>
292
+ <td style="color:var(--text-muted);font-size:0.8rem;">#${u.id}</td>
293
+ <td><strong>${u.firstName} ${u.lastName}</strong></td>
294
+ <td style="font-size:0.85rem;">${u.email}</td>
295
+ <td>${roleBadge}</td>
296
+ <td style="color:var(--text-muted);font-size:0.82rem;">${joined}</td>
297
+ <td>
298
+ <div class="flex gap-2">
299
+ <button class="btn btn-outline btn-sm" onclick="changeRole(${u.id},'${newRole}')">${toggleLabel}</button>
300
+ <button class="btn btn-danger btn-sm" onclick="deleteUser(${u.id},'${u.firstName}')">Delete</button>
301
+ </div>
302
+ </td>
303
+ </tr>`;
304
+ }).join('');
305
+ } catch (err) {
306
+ tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;padding:32px;color:var(--danger);">${err.message}</td></tr>`;
307
+ }
308
+ }
309
+
310
+ async function changeRole(id, role) {
311
+ try {
312
+ await api('PUT', `/api/v1/admin/users/${id}/role`, { role });
313
+ showToast('Role updated βœ“');
314
+ loadAdminUsers();
315
+ } catch (err) {
316
+ showToast('Failed: ' + err.message, 'error');
317
+ }
318
+ }
319
+
320
+ async function deleteUser(id, name) {
321
+ if (!confirm(`Delete user "${name}"? This cannot be undone.`)) return;
322
+ try {
323
+ await api('DELETE', `/api/v1/admin/users/${id}`);
324
+ showToast(`User "${name}" deleted.`);
325
+ loadAdminUsers();
326
+ } catch (err) {
327
+ showToast('Failed: ' + err.message, 'error');
328
+ }
329
+ }
330
+
331
+ // ----------------------------------------------------------------
332
+ // Boot
333
+ // ----------------------------------------------------------------
334
+ (function boot() {
335
+ if (Auth.loggedIn()) {
336
+ enterApp();
337
+ } else {
338
+ showPage('landing');
339
+ }
340
+ })();