diff options
Diffstat (limited to 'templates')
| -rw-r--r-- | templates/account.html | 23 | ||||
| -rw-r--r-- | templates/account_password.html | 35 | ||||
| -rw-r--r-- | templates/admin.html | 60 | ||||
| -rw-r--r-- | templates/css/account.css | 5 | ||||
| -rw-r--r-- | templates/css/admin.css | 57 | ||||
| -rw-r--r-- | templates/css/style.css | 128 | ||||
| -rw-r--r-- | templates/js/admin.js | 63 | ||||
| -rw-r--r-- | templates/js/auth.js | 48 | ||||
| -rw-r--r-- | templates/login.html | 34 | ||||
| -rw-r--r-- | templates/register.html | 38 |
10 files changed, 491 insertions, 0 deletions
diff --git a/templates/account.html b/templates/account.html new file mode 100644 index 0000000..0a1a353 --- /dev/null +++ b/templates/account.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>My Account</title> + <link rel="stylesheet" href="{{.Paths.Assets}}/css/style.css"> + <link rel="stylesheet" href="{{.Paths.Assets}}/css/account.css"> +</head> +<body> + <div class="container content-container"> + <h2>My Account</h2> + <p>Welcome, {{.User.Username}}!</p> + <ul> + <li><a href="{{.Paths.AccountPassword}}">Change Password</a></li> + </ul> + <div class="logout-link"> + <a href="{{.Paths.Logout}}">Logout</a> + </div> + </div> + <script src="{{.Paths.Assets}}/js/auth.js"></script> +</body> +</html> diff --git a/templates/account_password.html b/templates/account_password.html new file mode 100644 index 0000000..ba40761 --- /dev/null +++ b/templates/account_password.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Change Password</title> + <link rel="stylesheet" href="{{.Paths.Assets}}/css/style.css"> + <link rel="stylesheet" href="{{.Paths.Assets}}/css/account.css"> +</head> +<body> + <div class="container content-container"> + <h2>Change Password</h2> + <form id="change-password-form" action="{{.Paths.AccountPassword}}" method="post"> + <div class="form-group"> + <label for="current_password">Current Password</label> + <input type="password" id="current_password" name="current_password" required> + </div> + <div class="form-group"> + <label for="new_password">New Password</label> + <input type="password" id="new_password" name="new_password" required> + </div> + <div class="form-group"> + <label for="confirm_new_password">Confirm New Password</label> + <input type="password" id="confirm_new_password" name="confirm_new_password" required> + </div> + <button type="submit">Update Password</button> + </form> + <p id="message-placeholder" class="message"></p> + <div class="back-link"> + <a href="{{.Paths.Account}}">Back to Account</a> + </div> + </div> + <script src="{{.Paths.Assets}}/js/auth.js"></script> +</body> +</html> diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..ac4ba94 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Admin Dashboard</title> + <link rel="stylesheet" href="{{.Paths.Assets}}/css/style.css"> + <link rel="stylesheet" href="{{.Paths.Assets}}/css/admin.css"> +</head> +<body class="admin-page"> + <div class="container admin-container"> + <h1>User Management</h1> + <a href="{{.Paths.Account}}">My Account</a> | <a href="{{.Paths.Logout}}">Logout</a> + <div class="table-container"> + <table> + <thead> + <tr> + <th>ID</th> + <th>Username</th> + <th>Role</th> + <th>Is Active</th> + <th>Failed Logins</th> + <th>Last Login</th> + <th>Actions</th> + </tr> + </thead> + <tbody id="user-list"> + {{range .Users}} + <tr> + <td>{{.ID}}</td> + <td>{{.Username}}</td> + <td> + <select id="role-{{.ID}}"> + <option value="user" {{if eq .Role "user"}}selected{{end}}>user</option> + <option value="admin" {{if eq .Role "admin"}}selected{{end}}>admin</option> + </select> + </td> + <td>{{if .IsActive}}Yes{{else}}No{{end}}</td> + <td>{{.FailedLogins}}</td> + <td>{{if .LastLoginAt.Valid}}{{.LastLoginAt.Time.Format "2006-01-02 15:04"}}{{else}}Never{{end}}</td> + <td> + <button onclick="updateRole({{.ID}})">Save Role</button> + <button onclick="toggleActive({{.ID}}, {{not .IsActive}})">{{if .IsActive}}Deactivate{{else}}Activate{{end}}</button> + <button class="delete" onclick="deleteUser({{.ID}})">Delete</button> + </td> + </tr> + {{end}} + </tbody> + </table> + </div> + </div> + + <script> + const API_BASE_PATH = "{{.Paths.AdminUsersAPI}}"; + const LOGIN_PATH = "{{.Paths.Login}}"; + </script> + <script src="{{.Paths.Assets}}/js/auth.js"></script> + <script src="{{.Paths.Assets}}/js/admin.js"></script> +</body> +</html> diff --git a/templates/css/account.css b/templates/css/account.css new file mode 100644 index 0000000..f9617ec --- /dev/null +++ b/templates/css/account.css @@ -0,0 +1,5 @@ +/* Styles for account pages */ +.account-container { + width: 100%; + max-width: 450px; +} diff --git a/templates/css/admin.css b/templates/css/admin.css new file mode 100644 index 0000000..446b083 --- /dev/null +++ b/templates/css/admin.css @@ -0,0 +1,57 @@ +/* Admin page specific container */ +.admin-container { + max-width: 95%; + width: 1200px; + margin: 2rem auto; +} + +/* Responsive wrapper for the table */ +.table-container { + overflow-x: auto; + width: 100%; + margin-top: 1.5rem; + border: 1px solid #ddd; + border-radius: 8px; +} + +/* Admin Table */ +table { + width: 100%; + border-collapse: collapse; + min-width: 800px; /* Prevent table from becoming too cramped */ +} + +th, td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid #ddd; + white-space: nowrap; /* Prevent content from wrapping */ +} + +tr:last-child td { + border-bottom: none; +} + +th { + background-color: #f7f7f7; + font-weight: 600; +} + +td button { + margin-right: 0.5rem; + padding: 0.25rem 0.5rem; + width: auto; /* Override common button width */ + font-size: 0.875rem; +} + +td select { + padding: 0.25rem 0.5rem; +} + +button.delete { + background-color: #dc3545; + border-color: #dc3545; +} +button.delete:hover { + background-color: #c82333; +}
\ No newline at end of file diff --git a/templates/css/style.css b/templates/css/style.css new file mode 100644 index 0000000..dfe1139 --- /dev/null +++ b/templates/css/style.css @@ -0,0 +1,128 @@ +/* General Body Styles */ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f0f2f5; + margin: 0; + padding: 1rem; + box-sizing: border-box; +} + +/* General Container for forms and content */ +.container { + background-color: #fff; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; + box-sizing: border-box; +} + +/* Centered page content (login, register) */ +.form-container { + max-width: 420px; +} + +/* Wider content (account page) */ +.content-container { + max-width: 500px; +} + + +/* Headings */ +h1, h2 { + text-align: center; + margin-top: 0; + margin-bottom: 1.5rem; + color: #333; +} + +/* Forms */ +.form-group { + margin-bottom: 1rem; +} + +label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #555; +} + +input[type="text"], +input[type="password"], +input[type="email"] { + width: 100%; + padding: 0.75rem; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 1rem; +} + +/* Buttons */ +button { + width: 100%; + padding: 0.75rem; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + color: #fff; + background-color: #007bff; + transition: background-color 0.2s; +} + +button:hover { + background-color: #0056b3; +} + +button.btn-success { + background-color: #28a745; +} +button.btn-success:hover { + background-color: #218838; +} + +/* Links */ +.form-link { + text-align: center; + margin-top: 1rem; +} + +a { + text-decoration: none; + color: #007bff; +} + +a:hover { + text-decoration: underline; +} + +/* Messages */ +.error, .message.error { + color: #dc3545; + text-align: center; + margin-top: 1rem; + min-height: 1em; +} + +.message.success { + color: #28a745; + text-align: center; + margin-top: 1rem; +} + +/* Lists */ +ul { + list-style: none; + padding: 0; +} + +li { + margin-bottom: 1rem; +}
\ No newline at end of file diff --git a/templates/js/admin.js b/templates/js/admin.js new file mode 100644 index 0000000..4dff572 --- /dev/null +++ b/templates/js/admin.js @@ -0,0 +1,63 @@ +/** + * A wrapper for the fetch API that standardizes API calls. + * It automatically sets the Content-Type for POST requests + * and handles non-OK responses. + * @param {string} url The URL to fetch. + * @param {object} options The options for the fetch call. + * @returns {Promise<Response>} + */ +async function fetchApi(url, options = {}) { + options.headers = { + ...options.headers, + }; + + // Set content type for methods that have a body. + if (options.method === 'POST' || options.method === 'PUT') { + options.headers['Content-Type'] = 'application/json'; + } + + const response = await fetch(url, options); + + // Redirect to login if unauthorized. + if (response.status === 401) { + window.location.href = LOGIN_PATH; + // Throw an error to stop further processing + throw new Error('Unauthorized'); + } + + return response; +} + +function updateRole(userId) { + const role = document.getElementById(`role-${userId}`).value; + fetchApi(`${API_BASE_PATH}/${userId}/role`, { + method: 'POST', + body: JSON.stringify({ role }) + }).then(handleResponse); +} + +function deleteUser(userId) { + if (confirm('Are you sure you want to permanently delete this user?')) { + fetchApi(`${API_BASE_PATH}/${userId}`, { + method: 'DELETE' + }).then(handleResponse); + } +} + +function toggleActive(userId, isActive) { + const action = isActive ? 'activate' : 'deactivate'; + if (confirm(`Are you sure you want to ${action} this user?`)) { + fetchApi(`${API_BASE_PATH}/${userId}/status`, { + method: 'POST', + body: JSON.stringify({ is_active: isActive }) + }).then(handleResponse); + } +} + +function handleResponse(response) { + if (response.ok) { + location.reload(); + } else { + alert('Operation failed.'); + } +} diff --git a/templates/js/auth.js b/templates/js/auth.js new file mode 100644 index 0000000..3ca7c4e --- /dev/null +++ b/templates/js/auth.js @@ -0,0 +1,48 @@ +document.addEventListener('DOMContentLoaded', () => { + const changePasswordForm = document.getElementById('change-password-form'); + if (changePasswordForm) { + changePasswordForm.addEventListener('submit', handleChangePassword); + } +}); + +async function handleChangePassword(event) { + event.preventDefault(); + const form = event.target; + const formData = new FormData(form); + const messagePlaceholder = document.getElementById('message-placeholder'); + messagePlaceholder.textContent = ''; + + const newPassword = formData.get('new_password'); + const confirmNewPassword = formData.get('confirm_new_password'); + + if (newPassword !== confirmNewPassword) { + messagePlaceholder.className = 'error'; + messagePlaceholder.textContent = 'New passwords do not match.'; + return; + } + + try { + const response = await fetch(form.action, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams(formData) + }); + + const data = await response.json(); + + if (!response.ok) { + messagePlaceholder.className = 'error'; + messagePlaceholder.textContent = data.error || `Error: ${response.statusText}`; + } else { + messagePlaceholder.className = 'success'; + messagePlaceholder.textContent = data.message || 'Password updated successfully.'; + } + + } catch (error) { + console.error('Password change failed:', error); + messagePlaceholder.className = 'error'; + messagePlaceholder.textContent = 'An unexpected error occurred.'; + } +}
\ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..8875cb1 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Login</title> + <link rel="stylesheet" href="{{.Paths.Assets}}/css/style.css"> +</head> +<body> + <div class="container form-container"> + <h2>Login Required</h2> + <form id="login-form" action="{{.Paths.Login}}" method="post"> + <div class="form-group"> + <label for="username">Username</label> + <input type="text" id="username" name="username" required> + </div> + <div class="form-group"> + <label for="password">Password</label> + <input type="password" id="password" name="password" required> + </div> + <button type="submit">Login</button> + </form> + <p class="error"> + {{if .Error}} + {{.Error}} + {{end}} + </p> + <div style="text-align: center; margin-top: 1rem;"> + <a href="{{.Paths.Register}}">Don't have an account? Register</a> + </div> + </div> + <script src="{{.Paths.Assets}}/js/auth.js"></script> +</body> +</html> diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..d98ef4d --- /dev/null +++ b/templates/register.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Register</title> + <link rel="stylesheet" href="{{.Paths.Assets}}/css/style.css"> +</head> +<body> + <div class="container form-container"> + <h2>Create Account</h2> + <form id="register-form" action="{{.Paths.Register}}" method="post"> + <div class="form-group"> + <label for="username">Username</label> + <input type="text" id="username" name="username" required> + </div> + <div class="form-group"> + <label for="password">Password</label> + <input type="password" id="password" name="password" required> + </div> + <div class="form-group"> + <label for="confirm_password">Confirm Password</label> + <input type="password" id="confirm_password" name="confirm_password" required> + </div> + <button type="submit" class="btn-success">Register</button> + </form> + <p class="error"> + {{if .Error}} + {{.Error}} + {{end}} + </p> + <div class="form-link"> + <a href="{{.Paths.Login}}">Already have an account? Login</a> + </div> + </div> + <script src="{{.Paths.Assets}}/js/auth.js"></script> +</body> +</html> |
