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.
- package/README.md +80 -0
- package/dist/cli.js +55 -0
- package/package.json +53 -0
- package/templates/auth/AuthController.java.ejs +40 -0
- package/templates/auth/JwtAuthFilter.java.ejs +62 -0
- package/templates/auth/JwtService.java.ejs +81 -0
- package/templates/auth/SecurityConfig.java.ejs +65 -0
- package/templates/auth/UserDetailsServiceImpl.java.ejs +24 -0
- package/templates/entity/Entity.java.ejs +40 -0
- package/templates/entity/EntityController.java.ejs +92 -0
- package/templates/entity/EntityControllerTest.java.ejs +24 -0
- package/templates/entity/EntityDto.java.ejs +32 -0
- package/templates/entity/EntityRepository.java.ejs +9 -0
- package/templates/entity/EntityService.java.ejs +32 -0
- package/templates/project/java/config/CorsConfig.java.ejs +24 -0
- package/templates/project/java/config/SecurityConfig.java.ejs +76 -0
- package/templates/project/java/config/SecurityConfigSession.java.ejs +73 -0
- package/templates/project/java/config/SwaggerConfig.java.ejs +31 -0
- package/templates/project/java/controller/AdminController.java.ejs +82 -0
- package/templates/project/java/controller/AuthController.java.ejs +86 -0
- package/templates/project/java/controller/HomeController.java.ejs +63 -0
- package/templates/project/java/controller/ProfileController.java.ejs +65 -0
- package/templates/project/java/controller/UserController.java.ejs +35 -0
- package/templates/project/java/dto/AuthRequest.java.ejs +15 -0
- package/templates/project/java/dto/AuthResponse.java.ejs +18 -0
- package/templates/project/java/dto/ProfileUpdateRequest.java.ejs +20 -0
- package/templates/project/java/dto/RegisterRequest.java.ejs +30 -0
- package/templates/project/java/dto/UserDto.java.ejs +17 -0
- package/templates/project/java/entity/Role.java.ejs +6 -0
- package/templates/project/java/entity/User.java.ejs +97 -0
- package/templates/project/java/repository/UserRepository.java.ejs +11 -0
- package/templates/project/java/security/JwtAuthFilter.java.ejs +62 -0
- package/templates/project/java/security/UserDetailsServiceImpl.java.ejs +21 -0
- package/templates/project/java/service/JwtService.java.ejs +81 -0
- package/templates/project/java/service/UserService.java.ejs +32 -0
- package/templates/project/resources/application.yml.ejs +50 -0
- package/templates/project/resources/logback-spring.xml.ejs +41 -0
- package/templates/project/static/admin.html.ejs +163 -0
- package/templates/project/static/assets/app.js.ejs +340 -0
- package/templates/project/static/assets/style.css +533 -0
- package/templates/project/static/css/style.css +595 -0
- package/templates/project/static/dashboard.html.ejs +119 -0
- package/templates/project/static/index.html.ejs +96 -0
- package/templates/project/static/js/api.js +30 -0
- package/templates/project/static/js/auth.js +44 -0
- package/templates/project/static/js/nav.js.ejs +82 -0
- package/templates/project/static/js/ui.js +57 -0
- package/templates/project/static/login.html.ejs +71 -0
- package/templates/project/static/profile.html.ejs +163 -0
- package/templates/project/static/register.html.ejs +82 -0
- package/templates/project/thymeleaf/admin/users.html.ejs +111 -0
- package/templates/project/thymeleaf/dashboard.html.ejs +109 -0
- package/templates/project/thymeleaf/layout.html.ejs +75 -0
- package/templates/project/thymeleaf/login.html.ejs +56 -0
- package/templates/project/thymeleaf/profile.html.ejs +133 -0
- package/templates/project/thymeleaf/register.html.ejs +56 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
6
|
+
<title><%= projectName %></title>
|
|
7
|
+
<meta name="description" content="<%= description %>"/>
|
|
8
|
+
<link rel="stylesheet" href="/css/style.css"/>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="landing">
|
|
12
|
+
|
|
13
|
+
<!-- Top nav -->
|
|
14
|
+
<nav class="landing-nav">
|
|
15
|
+
<div class="landing-brand">
|
|
16
|
+
<span class="brand-mark">SG</span>
|
|
17
|
+
<span class="brand-name"><%= projectName %></span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="landing-nav-actions">
|
|
20
|
+
<a href="/login.html" class="btn btn-ghost btn-sm">Sign in</a>
|
|
21
|
+
<a href="/register.html" class="btn btn-primary btn-sm">Get started</a>
|
|
22
|
+
</div>
|
|
23
|
+
</nav>
|
|
24
|
+
|
|
25
|
+
<!-- Hero -->
|
|
26
|
+
<section class="landing-hero-section">
|
|
27
|
+
<div class="landing-container">
|
|
28
|
+
<div class="status-pill">
|
|
29
|
+
<span class="status-dot"></span>
|
|
30
|
+
<span>Running on Spring Boot 3.x</span>
|
|
31
|
+
</div>
|
|
32
|
+
<h1 class="hero-title">
|
|
33
|
+
Your Spring Boot backend<br/>
|
|
34
|
+
<span class="text-green">is ready.</span>
|
|
35
|
+
</h1>
|
|
36
|
+
<p class="hero-sub">
|
|
37
|
+
Generated by Sprygen. JWT authentication, role-based access control,
|
|
38
|
+
user management and an admin panel — all included.
|
|
39
|
+
</p>
|
|
40
|
+
<div class="hero-actions">
|
|
41
|
+
<a href="/register.html" class="btn btn-primary">Create account</a>
|
|
42
|
+
<a href="/login.html" class="btn btn-outline">Sign in</a>
|
|
43
|
+
<a href="/swagger-ui/index.html" class="btn btn-ghost" target="_blank">API Docs</a>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="tech-pills">
|
|
46
|
+
<span class="tech-pill">Spring Boot 3.x</span>
|
|
47
|
+
<span class="tech-pill">JWT</span>
|
|
48
|
+
<span class="tech-pill">RBAC</span>
|
|
49
|
+
<span class="tech-pill">Spring Data JPA</span>
|
|
50
|
+
<span class="tech-pill">BCrypt</span>
|
|
51
|
+
<span class="tech-pill">Validation</span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</section>
|
|
55
|
+
|
|
56
|
+
<!-- API reference -->
|
|
57
|
+
<section class="landing-section">
|
|
58
|
+
<div class="landing-container">
|
|
59
|
+
<div class="section-row">
|
|
60
|
+
<h2 class="section-heading">API Endpoints</h2>
|
|
61
|
+
<a href="/swagger-ui/index.html" class="link-green" target="_blank">Open Swagger UI</a>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="table-wrap">
|
|
64
|
+
<table>
|
|
65
|
+
<thead>
|
|
66
|
+
<tr><th>Method</th><th>Endpoint</th><th>Description</th><th>Access</th></tr>
|
|
67
|
+
</thead>
|
|
68
|
+
<tbody>
|
|
69
|
+
<tr><td><code class="http-post">POST</code></td><td><code>/api/v1/auth/register</code></td><td>Register new account</td><td><span class="badge badge-open">Public</span></td></tr>
|
|
70
|
+
<tr><td><code class="http-post">POST</code></td><td><code>/api/v1/auth/login</code></td><td>Login and receive JWT</td><td><span class="badge badge-open">Public</span></td></tr>
|
|
71
|
+
<tr><td><code class="http-get">GET</code></td><td><code>/api/v1/profile</code></td><td>Get own profile</td><td><span class="badge badge-auth">Auth</span></td></tr>
|
|
72
|
+
<tr><td><code class="http-put">PUT</code></td><td><code>/api/v1/profile</code></td><td>Update profile</td><td><span class="badge badge-auth">Auth</span></td></tr>
|
|
73
|
+
<tr><td><code class="http-put">PUT</code></td><td><code>/api/v1/profile/password</code></td><td>Change password</td><td><span class="badge badge-auth">Auth</span></td></tr>
|
|
74
|
+
<tr><td><code class="http-get">GET</code></td><td><code>/api/v1/admin/users</code></td><td>List all users</td><td><span class="badge badge-admin">Admin</span></td></tr>
|
|
75
|
+
<tr><td><code class="http-put">PUT</code></td><td><code>/api/v1/admin/users/{id}/role</code></td><td>Assign role to user</td><td><span class="badge badge-admin">Admin</span></td></tr>
|
|
76
|
+
<tr><td><code class="http-delete">DELETE</code></td><td><code>/api/v1/admin/users/{id}</code></td><td>Delete user</td><td><span class="badge badge-admin">Admin</span></td></tr>
|
|
77
|
+
<tr><td><code class="http-get">GET</code></td><td><code>/actuator/health</code></td><td>Health check</td><td><span class="badge badge-open">Public</span></td></tr>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
|
|
84
|
+
<footer class="landing-footer">
|
|
85
|
+
<span>Generated with Sprygen</span>
|
|
86
|
+
<span><%= projectName %></span>
|
|
87
|
+
</footer>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<script src="/js/auth.js"></script>
|
|
91
|
+
<script>
|
|
92
|
+
// Redirect logged-in users to dashboard
|
|
93
|
+
if (Auth.loggedIn()) window.location.href = '/dashboard.html';
|
|
94
|
+
</script>
|
|
95
|
+
</body>
|
|
96
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* ================================================================
|
|
2
|
+
api.js — HTTP client with JWT injection
|
|
3
|
+
================================================================ */
|
|
4
|
+
|
|
5
|
+
const API_BASE = '';
|
|
6
|
+
|
|
7
|
+
export async function request(method, path, body) {
|
|
8
|
+
const token = Auth.token();
|
|
9
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
10
|
+
if (token) headers['Authorization'] = 'Bearer ' + token;
|
|
11
|
+
|
|
12
|
+
const res = await fetch(API_BASE + path, {
|
|
13
|
+
method,
|
|
14
|
+
headers,
|
|
15
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (res.status === 204) return {};
|
|
19
|
+
const json = await res.json().catch(() => ({}));
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const msg = json.message || json.error || `Request failed (${res.status})`;
|
|
22
|
+
throw new Error(msg);
|
|
23
|
+
}
|
|
24
|
+
return json;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const get = (path) => request('GET', path);
|
|
28
|
+
export const post = (path, body) => request('POST', path, body);
|
|
29
|
+
export const put = (path, body) => request('PUT', path, body);
|
|
30
|
+
export const del = (path) => request('DELETE', path);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* ================================================================
|
|
2
|
+
auth.js — JWT + localStorage session management
|
|
3
|
+
================================================================ */
|
|
4
|
+
|
|
5
|
+
const KEYS = { token: '_sg_token', user: '_sg_user' };
|
|
6
|
+
|
|
7
|
+
const Auth = window.Auth = {
|
|
8
|
+
save(data) {
|
|
9
|
+
localStorage.setItem(KEYS.token, data.token);
|
|
10
|
+
localStorage.setItem(KEYS.user, JSON.stringify({
|
|
11
|
+
email: data.email || '',
|
|
12
|
+
firstName: data.firstName || '',
|
|
13
|
+
lastName: data.lastName || '',
|
|
14
|
+
role: data.role || 'ROLE_USER',
|
|
15
|
+
}));
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
clear() {
|
|
19
|
+
localStorage.removeItem(KEYS.token);
|
|
20
|
+
localStorage.removeItem(KEYS.user);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
token() { return localStorage.getItem(KEYS.token); },
|
|
24
|
+
|
|
25
|
+
user() {
|
|
26
|
+
try { return JSON.parse(localStorage.getItem(KEYS.user) || 'null'); }
|
|
27
|
+
catch { return null; }
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
isAdmin() { const u = this.user(); return u?.role === 'ROLE_ADMIN'; },
|
|
31
|
+
loggedIn() { return !!this.token() && !!this.user(); },
|
|
32
|
+
|
|
33
|
+
initials() {
|
|
34
|
+
const u = this.user();
|
|
35
|
+
if (!u) return '??';
|
|
36
|
+
return ((u.firstName?.[0] || '') + (u.lastName?.[0] || '')).toUpperCase();
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
fullName() {
|
|
40
|
+
const u = this.user();
|
|
41
|
+
if (!u) return '';
|
|
42
|
+
return u.firstName + ' ' + u.lastName;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* ================================================================
|
|
2
|
+
nav.js — Sidebar + topbar component
|
|
3
|
+
================================================================ */
|
|
4
|
+
|
|
5
|
+
function renderNav(activePage) {
|
|
6
|
+
const user = Auth.user();
|
|
7
|
+
const isAdmin = Auth.isAdmin();
|
|
8
|
+
|
|
9
|
+
const adminSection = isAdmin ? `
|
|
10
|
+
<li class="nav-section-label">Administration</li>
|
|
11
|
+
<li>
|
|
12
|
+
<a class="nav-link ${activePage === 'admin' ? 'active' : ''}"
|
|
13
|
+
href="/admin.html">
|
|
14
|
+
<span class="nav-icon">A</span> Admin Panel
|
|
15
|
+
<span class="nav-badge">ADMIN</span>
|
|
16
|
+
</a>
|
|
17
|
+
</li>` : '';
|
|
18
|
+
|
|
19
|
+
document.getElementById('sidebar').innerHTML = `
|
|
20
|
+
<div class="sidebar-brand">
|
|
21
|
+
<span class="brand-mark">SG</span>
|
|
22
|
+
<span class="brand-name"><%= projectName %></span>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<nav class="sidebar-nav">
|
|
26
|
+
<ul>
|
|
27
|
+
<li class="nav-section-label">Menu</li>
|
|
28
|
+
<li>
|
|
29
|
+
<a class="nav-link ${activePage === 'dashboard' ? 'active' : ''}"
|
|
30
|
+
href="/dashboard.html">
|
|
31
|
+
<span class="nav-icon">D</span> Dashboard
|
|
32
|
+
</a>
|
|
33
|
+
</li>
|
|
34
|
+
<li>
|
|
35
|
+
<a class="nav-link ${activePage === 'profile' ? 'active' : ''}"
|
|
36
|
+
href="/profile.html">
|
|
37
|
+
<span class="nav-icon">P</span> Profile
|
|
38
|
+
</a>
|
|
39
|
+
</li>
|
|
40
|
+
${adminSection}
|
|
41
|
+
</ul>
|
|
42
|
+
</nav>
|
|
43
|
+
|
|
44
|
+
<div class="sidebar-footer">
|
|
45
|
+
<div class="user-chip">
|
|
46
|
+
<div class="avatar">${Auth.initials()}</div>
|
|
47
|
+
<div class="user-chip-info">
|
|
48
|
+
<div class="user-chip-name">${Auth.fullName()}</div>
|
|
49
|
+
<div class="user-chip-role">${isAdmin ? 'Administrator' : 'Member'}</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<button class="btn btn-ghost btn-sm btn-full" onclick="doLogout()">Sign out</button>
|
|
53
|
+
</div>
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function doLogout() {
|
|
58
|
+
Auth.clear();
|
|
59
|
+
window.location.href = '/';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function guardAuth() {
|
|
63
|
+
if (!Auth.loggedIn()) {
|
|
64
|
+
window.location.href = '/login.html';
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function guardAdmin() {
|
|
71
|
+
if (!guardAuth()) return false;
|
|
72
|
+
if (!Auth.isAdmin()) {
|
|
73
|
+
window.location.href = '/dashboard.html';
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
window.renderNav = renderNav;
|
|
80
|
+
window.doLogout = doLogout;
|
|
81
|
+
window.guardAuth = guardAuth;
|
|
82
|
+
window.guardAdmin = guardAdmin;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* ================================================================
|
|
2
|
+
ui.js — Toast, alerts, counters, loaders
|
|
3
|
+
================================================================ */
|
|
4
|
+
|
|
5
|
+
// ── Toast ─────────────────────────────────────────────────────
|
|
6
|
+
let _toastTimer;
|
|
7
|
+
function showToast(msg, type = 'success') {
|
|
8
|
+
const el = document.getElementById('toast');
|
|
9
|
+
if (!el) return;
|
|
10
|
+
el.textContent = msg;
|
|
11
|
+
el.className = 'toast toast-' + type + ' show';
|
|
12
|
+
clearTimeout(_toastTimer);
|
|
13
|
+
_toastTimer = setTimeout(() => el.classList.remove('show'), 3200);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── Alert inside a form ───────────────────────────────────────
|
|
17
|
+
function showAlert(id, msg, type = 'error') {
|
|
18
|
+
const el = document.getElementById(id);
|
|
19
|
+
if (!el) return;
|
|
20
|
+
el.textContent = msg;
|
|
21
|
+
el.className = 'alert alert-' + type + ' visible';
|
|
22
|
+
}
|
|
23
|
+
function clearAlert(id) {
|
|
24
|
+
const el = document.getElementById(id);
|
|
25
|
+
if (el) el.className = 'alert';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Animated counter ─────────────────────────────────────────
|
|
29
|
+
function animateCount(el, target, duration = 800) {
|
|
30
|
+
if (!el) return;
|
|
31
|
+
const start = 0;
|
|
32
|
+
const step = target / (duration / 16);
|
|
33
|
+
let current = start;
|
|
34
|
+
const timer = setInterval(() => {
|
|
35
|
+
current = Math.min(current + step, target);
|
|
36
|
+
el.textContent = Math.round(current).toLocaleString();
|
|
37
|
+
if (current >= target) clearInterval(timer);
|
|
38
|
+
}, 16);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Busy button ───────────────────────────────────────────────
|
|
42
|
+
function setBusy(btn, busy, label) {
|
|
43
|
+
btn.disabled = busy;
|
|
44
|
+
if (busy) {
|
|
45
|
+
btn.dataset.orig = btn.textContent;
|
|
46
|
+
btn.textContent = label || 'Working…';
|
|
47
|
+
} else {
|
|
48
|
+
btn.textContent = btn.dataset.orig || btn.textContent;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// expose globally
|
|
53
|
+
window.showToast = showToast;
|
|
54
|
+
window.showAlert = showAlert;
|
|
55
|
+
window.clearAlert = clearAlert;
|
|
56
|
+
window.animateCount = animateCount;
|
|
57
|
+
window.setBusy = setBusy;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
6
|
+
<title>Sign in — <%= projectName %></title>
|
|
7
|
+
<link rel="stylesheet" href="/css/style.css"/>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="auth-body">
|
|
10
|
+
<div class="auth-wrap">
|
|
11
|
+
<div class="auth-card">
|
|
12
|
+
<div class="auth-header">
|
|
13
|
+
<div class="brand-mark-lg">SG</div>
|
|
14
|
+
<h1><%= projectName %></h1>
|
|
15
|
+
<p class="text-muted">Sign in to your account</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div id="login-alert" class="alert"></div>
|
|
19
|
+
|
|
20
|
+
<form id="login-form">
|
|
21
|
+
<div class="form-group">
|
|
22
|
+
<label for="email">Email address</label>
|
|
23
|
+
<input id="email" type="email" placeholder="you@example.com" required autocomplete="email"/>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="form-group">
|
|
26
|
+
<label for="password">Password</label>
|
|
27
|
+
<input id="password" type="password" placeholder="••••••••" required autocomplete="current-password"/>
|
|
28
|
+
</div>
|
|
29
|
+
<button type="submit" id="submit-btn" class="btn btn-primary btn-full">Sign in</button>
|
|
30
|
+
</form>
|
|
31
|
+
|
|
32
|
+
<p class="auth-switch">
|
|
33
|
+
New here? <a href="/register.html">Create an account</a>
|
|
34
|
+
</p>
|
|
35
|
+
<p class="auth-switch">
|
|
36
|
+
<a href="/" class="text-dim">← Back to home</a>
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<script src="/js/auth.js"></script>
|
|
41
|
+
<script src="/js/ui.js"></script>
|
|
42
|
+
<script>
|
|
43
|
+
// Redirect if already logged in
|
|
44
|
+
if (Auth.loggedIn()) window.location.href = '/dashboard.html';
|
|
45
|
+
|
|
46
|
+
document.getElementById('login-form').addEventListener('submit', async e => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
clearAlert('login-alert');
|
|
49
|
+
const btn = document.getElementById('submit-btn');
|
|
50
|
+
setBusy(btn, true, 'Signing in…');
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch('/api/v1/auth/login', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
email: document.getElementById('email').value,
|
|
57
|
+
password: document.getElementById('password').value,
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
const data = await res.json();
|
|
61
|
+
if (!res.ok) throw new Error(data.message || 'Invalid credentials');
|
|
62
|
+
Auth.save(data);
|
|
63
|
+
window.location.href = '/dashboard.html';
|
|
64
|
+
} catch (err) {
|
|
65
|
+
showAlert('login-alert', err.message);
|
|
66
|
+
setBusy(btn, false);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
6
|
+
<title><%= projectName %> — Profile</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/style.css"/>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="app-shell">
|
|
11
|
+
<aside id="sidebar" class="sidebar"></aside>
|
|
12
|
+
<div class="main">
|
|
13
|
+
<header class="topbar">
|
|
14
|
+
<span class="topbar-title">Profile</span>
|
|
15
|
+
</header>
|
|
16
|
+
<div class="page">
|
|
17
|
+
|
|
18
|
+
<div class="profile-hero" id="profile-hero">
|
|
19
|
+
<div class="avatar avatar-lg" id="hero-avatar"></div>
|
|
20
|
+
<div>
|
|
21
|
+
<h2 id="hero-name"></h2>
|
|
22
|
+
<p class="text-muted" id="hero-email"></p>
|
|
23
|
+
<span id="hero-badge" class="badge" style="margin-top:6px;display:inline-block;"></span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="two-col">
|
|
28
|
+
<!-- Personal info -->
|
|
29
|
+
<div class="card">
|
|
30
|
+
<div class="card-header"><span class="card-title">Personal information</span></div>
|
|
31
|
+
<div class="card-body">
|
|
32
|
+
<div id="profile-alert" class="alert"></div>
|
|
33
|
+
<form id="profile-form">
|
|
34
|
+
<div class="row-2">
|
|
35
|
+
<div class="form-group">
|
|
36
|
+
<label>First name</label>
|
|
37
|
+
<input id="pf-first" type="text"/>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="form-group">
|
|
40
|
+
<label>Last name</label>
|
|
41
|
+
<input id="pf-last" type="text"/>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="form-group">
|
|
45
|
+
<label>Bio</label>
|
|
46
|
+
<textarea id="pf-bio" rows="3" placeholder="A short bio…"></textarea>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="form-group">
|
|
49
|
+
<label>Avatar URL</label>
|
|
50
|
+
<input id="pf-avatar" type="url" placeholder="https://…"/>
|
|
51
|
+
</div>
|
|
52
|
+
<button type="submit" class="btn btn-primary" id="save-btn">Save changes</button>
|
|
53
|
+
</form>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Change password -->
|
|
58
|
+
<div class="card">
|
|
59
|
+
<div class="card-header"><span class="card-title">Change password</span></div>
|
|
60
|
+
<div class="card-body">
|
|
61
|
+
<div id="pw-alert" class="alert"></div>
|
|
62
|
+
<form id="password-form">
|
|
63
|
+
<div class="form-group">
|
|
64
|
+
<label>Current password</label>
|
|
65
|
+
<input id="pw-current" type="password" placeholder="••••••••"/>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="form-group">
|
|
68
|
+
<label>New password</label>
|
|
69
|
+
<input id="pw-new" type="password" placeholder="••••••••"/>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="form-group">
|
|
72
|
+
<label>Confirm new password</label>
|
|
73
|
+
<input id="pw-confirm" type="password" placeholder="••••••••"/>
|
|
74
|
+
</div>
|
|
75
|
+
<button type="submit" class="btn btn-primary" id="pw-btn">Update password</button>
|
|
76
|
+
</form>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div id="toast" class="toast"></div>
|
|
84
|
+
<script src="/js/auth.js"></script>
|
|
85
|
+
<script src="/js/ui.js"></script>
|
|
86
|
+
<script src="/js/nav.js"></script>
|
|
87
|
+
<script>
|
|
88
|
+
if (!guardAuth()) throw new Error('redirect');
|
|
89
|
+
renderNav('profile');
|
|
90
|
+
|
|
91
|
+
async function loadProfile() {
|
|
92
|
+
const res = await fetch('/api/v1/profile', {
|
|
93
|
+
headers: { Authorization: 'Bearer ' + Auth.token() }
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) { showToast('Failed to load profile', 'error'); return; }
|
|
96
|
+
const u = await res.json();
|
|
97
|
+
|
|
98
|
+
const initials = ((u.firstName?.[0]||'') + (u.lastName?.[0]||'')).toUpperCase();
|
|
99
|
+
document.getElementById('hero-avatar').textContent = initials;
|
|
100
|
+
document.getElementById('hero-name').textContent = u.firstName + ' ' + u.lastName;
|
|
101
|
+
document.getElementById('hero-email').textContent = u.email;
|
|
102
|
+
|
|
103
|
+
const isAdmin = u.roles?.includes('ROLE_ADMIN');
|
|
104
|
+
const badge = document.getElementById('hero-badge');
|
|
105
|
+
badge.textContent = isAdmin ? 'Administrator' : 'Member';
|
|
106
|
+
badge.className = 'badge ' + (isAdmin ? 'badge-admin' : 'badge-auth');
|
|
107
|
+
|
|
108
|
+
document.getElementById('pf-first').value = u.firstName || '';
|
|
109
|
+
document.getElementById('pf-last').value = u.lastName || '';
|
|
110
|
+
document.getElementById('pf-bio').value = u.bio || '';
|
|
111
|
+
document.getElementById('pf-avatar').value = u.avatarUrl || '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
document.getElementById('profile-form').addEventListener('submit', async e => {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
clearAlert('profile-alert');
|
|
117
|
+
const btn = document.getElementById('save-btn');
|
|
118
|
+
setBusy(btn, true, 'Saving…');
|
|
119
|
+
try {
|
|
120
|
+
await fetch('/api/v1/profile', {
|
|
121
|
+
method: 'PUT',
|
|
122
|
+
headers: { 'Content-Type':'application/json', Authorization: 'Bearer ' + Auth.token() },
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
firstName: document.getElementById('pf-first').value,
|
|
125
|
+
lastName: document.getElementById('pf-last').value,
|
|
126
|
+
bio: document.getElementById('pf-bio').value,
|
|
127
|
+
avatarUrl: document.getElementById('pf-avatar').value,
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
showToast('Profile updated');
|
|
131
|
+
loadProfile();
|
|
132
|
+
} catch(err) {
|
|
133
|
+
showAlert('profile-alert', err.message);
|
|
134
|
+
} finally { setBusy(btn, false); }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
document.getElementById('password-form').addEventListener('submit', async e => {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
clearAlert('pw-alert');
|
|
140
|
+
const newPw = document.getElementById('pw-new').value;
|
|
141
|
+
const confirm = document.getElementById('pw-confirm').value;
|
|
142
|
+
if (newPw !== confirm) { showAlert('pw-alert', 'Passwords do not match.'); return; }
|
|
143
|
+
const btn = document.getElementById('pw-btn');
|
|
144
|
+
setBusy(btn, true, 'Updating…');
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetch('/api/v1/profile/password', {
|
|
147
|
+
method: 'PUT',
|
|
148
|
+
headers: { 'Content-Type':'application/json', Authorization: 'Bearer ' + Auth.token() },
|
|
149
|
+
body: JSON.stringify({ currentPassword: document.getElementById('pw-current').value, newPassword: newPw })
|
|
150
|
+
});
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
if (!res.ok) throw new Error(data.error || 'Failed');
|
|
153
|
+
document.getElementById('password-form').reset();
|
|
154
|
+
showToast('Password changed');
|
|
155
|
+
} catch(err) {
|
|
156
|
+
showAlert('pw-alert', err.message);
|
|
157
|
+
} finally { setBusy(btn, false); }
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
loadProfile();
|
|
161
|
+
</script>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8"/>
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
6
|
+
<title>Register — <%= projectName %></title>
|
|
7
|
+
<link rel="stylesheet" href="/css/style.css"/>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="auth-body">
|
|
10
|
+
<div class="auth-wrap">
|
|
11
|
+
<div class="auth-card">
|
|
12
|
+
<div class="auth-header">
|
|
13
|
+
<div class="brand-mark-lg">SG</div>
|
|
14
|
+
<h1><%= projectName %></h1>
|
|
15
|
+
<p class="text-muted">Create your account</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div id="reg-alert" class="alert"></div>
|
|
19
|
+
|
|
20
|
+
<form id="register-form">
|
|
21
|
+
<div class="row-2">
|
|
22
|
+
<div class="form-group">
|
|
23
|
+
<label for="firstName">First name</label>
|
|
24
|
+
<input id="firstName" type="text" placeholder="John" required/>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="form-group">
|
|
27
|
+
<label for="lastName">Last name</label>
|
|
28
|
+
<input id="lastName" type="text" placeholder="Doe" required/>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-group">
|
|
32
|
+
<label for="email">Email address</label>
|
|
33
|
+
<input id="email" type="email" placeholder="you@example.com" required autocomplete="email"/>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="form-group">
|
|
36
|
+
<label for="password">
|
|
37
|
+
Password
|
|
38
|
+
<span class="label-hint">Min 6 characters</span>
|
|
39
|
+
</label>
|
|
40
|
+
<input id="password" type="password" placeholder="••••••••" required minlength="6" autocomplete="new-password"/>
|
|
41
|
+
</div>
|
|
42
|
+
<button type="submit" id="submit-btn" class="btn btn-primary btn-full">Create account</button>
|
|
43
|
+
</form>
|
|
44
|
+
|
|
45
|
+
<p class="auth-switch">
|
|
46
|
+
Already have an account? <a href="/login.html">Sign in</a>
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<script src="/js/auth.js"></script>
|
|
51
|
+
<script src="/js/ui.js"></script>
|
|
52
|
+
<script>
|
|
53
|
+
if (Auth.loggedIn()) window.location.href = '/dashboard.html';
|
|
54
|
+
|
|
55
|
+
document.getElementById('register-form').addEventListener('submit', async e => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
clearAlert('reg-alert');
|
|
58
|
+
const btn = document.getElementById('submit-btn');
|
|
59
|
+
setBusy(btn, true, 'Creating account…');
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch('/api/v1/auth/register', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
firstName: document.getElementById('firstName').value,
|
|
66
|
+
lastName: document.getElementById('lastName').value,
|
|
67
|
+
email: document.getElementById('email').value,
|
|
68
|
+
password: document.getElementById('password').value,
|
|
69
|
+
})
|
|
70
|
+
});
|
|
71
|
+
const data = await res.json();
|
|
72
|
+
if (!res.ok) throw new Error(data.message || 'Registration failed');
|
|
73
|
+
Auth.save(data);
|
|
74
|
+
window.location.href = '/dashboard.html';
|
|
75
|
+
} catch (err) {
|
|
76
|
+
showAlert('reg-alert', err.message);
|
|
77
|
+
setBusy(btn, false);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|