summaryrefslogtreecommitdiff
path: root/templates
diff options
context:
space:
mode:
Diffstat (limited to 'templates')
-rw-r--r--templates/account.html23
-rw-r--r--templates/account_password.html35
-rw-r--r--templates/admin.html60
-rw-r--r--templates/css/account.css5
-rw-r--r--templates/css/admin.css57
-rw-r--r--templates/css/style.css128
-rw-r--r--templates/js/admin.js63
-rw-r--r--templates/js/auth.js48
-rw-r--r--templates/login.html34
-rw-r--r--templates/register.html38
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>