Revise ui to make happy pass work

This commit is contained in:
2025-08-04 22:52:34 +10:00
parent dda44b9b25
commit 35b3081a0a
8 changed files with 451 additions and 236 deletions
-73
View File
@@ -1,73 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- License changed from MIT to Apache 2.0 for better patent protection and enterprise adoption
### Added
- Initial release of Passkey Auth
- WebAuthn (FIDO2) authentication support
- SQLite database with user management
- Email-based user identification
- Admin approval workflow for new users
- Email allowlist support
- Session management with secure cookies
- Kubernetes deployment configuration
- nginx ingress auth backend support
- Docker containerization with Debian base
- Web-based admin interface
- User registration and login flows
- Real-time WebAuthn credential management
### Features
- **Authentication**: WebAuthn/FIDO2 passkey authentication
- **Database**: SQLite with user and credential storage
- **Admin Interface**: Web UI for user management and approval
- **Email Allowlist**: Restrict registration to specific email addresses
- **Session Management**: Secure session handling with configurable secrets
- **Docker Support**: Multi-stage builds with Debian base for SQLite compatibility
- **Kubernetes Ready**: Complete deployment manifests and ingress configuration
- **nginx Integration**: Auth backend for protecting upstream services
### Security
- WebAuthn challenge/response authentication
- Secure session cookie handling
- Admin approval workflow
- Email-based access control
- HTTPS-ready configuration
### Documentation
- Comprehensive README with setup instructions
- API endpoint documentation
- Kubernetes deployment guide
- Docker usage examples
- Troubleshooting guide
- Contributing guidelines
### Technical Details
- Go 1.21+ with CGO for SQLite
- Base64URL encoding/decoding for WebAuthn data
- CORS support for cross-origin requests
- Environment variable configuration
- Health check endpoints
- Structured logging
## [0.1.0] - 2025-08-04
### Added
- Initial project structure
- Core WebAuthn implementation
- SQLite database integration
- Basic web interface
- Docker containerization
- Kubernetes manifests
---
**Note**: This project follows semantic versioning. For upgrade instructions and breaking changes, see the README.md file.
+5
View File
@@ -53,6 +53,10 @@ auth:
# - "user1@yourcompany.com" # - "user1@yourcompany.com"
# - "user2@yourcompany.com" # - "user2@yourcompany.com"
# Admin email - user with this email will be auto-approved and can access admin panel
# Can also be set via ADMIN_EMAIL environment variable
admin_email: "" # Set this to your admin email address
# Environment-specific overrides can be set via environment variables: # Environment-specific overrides can be set via environment variables:
# - PORT: Server port # - PORT: Server port
# - HOST: Server host # - HOST: Server host
@@ -60,3 +64,4 @@ auth:
# - DATABASE_PATH: Database file path # - DATABASE_PATH: Database file path
# - SESSION_SECRET: Session encryption secret # - SESSION_SECRET: Session encryption secret
# - ALLOWED_EMAILS: Comma-separated list of allowed emails # - ALLOWED_EMAILS: Comma-separated list of allowed emails
# - ADMIN_EMAIL: Admin email address
+2
View File
@@ -22,3 +22,5 @@ auth:
allowed_emails: allowed_emails:
# - "admin@example.com" # - "admin@example.com"
# - "user@example.com" # - "user@example.com"
# Admin email - user with this email will be auto-approved and can access admin panel
admin_email: "" # Set this to your admin email or use ADMIN_EMAIL environment variable
+9
View File
@@ -40,6 +40,7 @@ type AuthConfig struct {
SessionSecret string `yaml:"session_secret"` SessionSecret string `yaml:"session_secret"`
RequireApproval bool `yaml:"require_approval"` RequireApproval bool `yaml:"require_approval"`
AllowedEmails []string `yaml:"allowed_emails"` AllowedEmails []string `yaml:"allowed_emails"`
AdminEmail string `yaml:"admin_email"`
} }
func Load() (*Config, error) { func Load() (*Config, error) {
@@ -113,6 +114,9 @@ func Load() (*Config, error) {
config.Auth.AllowedEmails[i] = strings.TrimSpace(email) config.Auth.AllowedEmails[i] = strings.TrimSpace(email)
} }
} }
if adminEmail := os.Getenv("ADMIN_EMAIL"); adminEmail != "" {
config.Auth.AdminEmail = adminEmail
}
return config, nil return config, nil
} }
@@ -135,6 +139,11 @@ func (c *Config) IsEmailAllowed(email string) bool {
return false return false
} }
// IsAdmin checks if an email address is the admin email
func (c *Config) IsAdmin(email string) bool {
return c.Auth.AdminEmail != "" && email == c.Auth.AdminEmail
}
// validateConfigPath ensures the config path is safe and doesn't allow path traversal // validateConfigPath ensures the config path is safe and doesn't allow path traversal
func validateConfigPath(path string) error { func validateConfigPath(path string) error {
// Clean the path and check for path traversal attempts // Clean the path and check for path traversal attempts
+17
View File
@@ -98,6 +98,23 @@ func (db *DB) CreateUser(email, displayName string) (*User, error) {
return db.GetUser(int(id)) return db.GetUser(int(id))
} }
func (db *DB) CreateUserWithApproval(email, displayName string, approved bool) (*User, error) {
result, err := db.conn.Exec(
"INSERT INTO users (email, display_name, approved) VALUES (?, ?, ?)",
email, displayName, approved,
)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
return db.GetUser(int(id))
}
func (db *DB) GetUser(id int) (*User, error) { func (db *DB) GetUser(id int) (*User, error) {
var user User var user User
err := db.conn.QueryRow( err := db.conn.QueryRow(
+97 -13
View File
@@ -94,13 +94,18 @@ func (h *Handlers) BeginRegistration(w http.ResponseWriter, r *http.Request) {
} }
// Create new user // Create new user
user, err := h.db.CreateUser(req.Email, req.DisplayName) isAdmin := h.config.IsAdmin(req.Email)
user, err := h.db.CreateUserWithApproval(req.Email, req.DisplayName, isAdmin)
if err != nil { if err != nil {
logrus.Errorf("Failed to create user: %v", err) logrus.Errorf("Failed to create user: %v", err)
h.writeError(w, "Failed to create user", http.StatusInternalServerError) h.writeError(w, "Failed to create user", http.StatusInternalServerError)
return return
} }
if isAdmin {
logrus.Infof("Admin user auto-approved: %s", req.Email)
}
webAuthnUser := &auth.WebAuthnUser{} webAuthnUser := &auth.WebAuthnUser{}
webAuthnUser.SetUser(user) webAuthnUser.SetUser(user)
@@ -190,7 +195,17 @@ func (h *Handlers) FinishRegistration(w http.ResponseWriter, r *http.Request) {
return return
} }
// Clear session // Set authenticated session after successful registration
authSession, _ := h.store.Get(r, "auth-session")
authSession.Values["authenticated"] = true
authSession.Values["user_id"] = user.ID
authSession.Values["user_email"] = user.Email
if err := authSession.Save(r, w); err != nil {
h.writeError(w, "Failed to save auth session", http.StatusInternalServerError)
return
}
// Clear webauthn session
session.Values["challenge"] = nil session.Values["challenge"] = nil
session.Values["user_id"] = nil session.Values["user_id"] = nil
if err := session.Save(r, w); err != nil { if err := session.Save(r, w); err != nil {
@@ -364,11 +379,55 @@ func (h *Handlers) AuthCheck(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
// GetAuthStatus returns the current authentication status
func (h *Handlers) GetAuthStatus(w http.ResponseWriter, r *http.Request) {
session, err := h.store.Get(r, "auth-session")
if err != nil {
h.writeError(w, "Failed to get session", http.StatusInternalServerError)
return
}
userEmail, ok := session.Values["user_email"].(string)
if !ok || userEmail == "" {
h.writeError(w, "Not authenticated", http.StatusUnauthorized)
return
}
// Get user details from database
user, err := h.db.GetUserByEmail(userEmail)
if err != nil {
h.writeError(w, "User not found", http.StatusNotFound)
return
}
// Check if user is approved
if !user.Approved {
h.writeError(w, "User not approved", http.StatusForbidden)
return
}
response := map[string]interface{}{
"authenticated": true,
"user": map[string]interface{}{
"id": user.ID,
"email": user.Email,
"display_name": user.DisplayName,
"approved": user.Approved,
"is_admin": h.config.IsAdmin(user.Email),
},
}
h.writeJSON(w, response)
}
// Admin endpoints // Admin endpoints
// ListUsers returns all users (admin endpoint) // ListUsers returns all users (admin endpoint)
func (h *Handlers) ListUsers(w http.ResponseWriter, r *http.Request) { func (h *Handlers) ListUsers(w http.ResponseWriter, r *http.Request) {
// TODO: Add admin authentication check if !h.requireAdmin(w, r) {
return
}
users, err := h.db.ListUsers() users, err := h.db.ListUsers()
if err != nil { if err != nil {
logrus.Errorf("Failed to list users: %v", err) logrus.Errorf("Failed to list users: %v", err)
@@ -381,7 +440,10 @@ func (h *Handlers) ListUsers(w http.ResponseWriter, r *http.Request) {
// CreateUser creates a new user (admin endpoint) // CreateUser creates a new user (admin endpoint)
func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) { func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) {
// TODO: Add admin authentication check if !h.requireAdmin(w, r) {
return
}
var req struct { var req struct {
Email string `json:"email"` Email string `json:"email"`
DisplayName string `json:"display_name"` DisplayName string `json:"display_name"`
@@ -399,7 +461,7 @@ func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := h.db.CreateUser(req.Email, req.DisplayName) user, err := h.db.CreateUserWithApproval(req.Email, req.DisplayName, req.Approved)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") { if strings.Contains(err.Error(), "UNIQUE constraint failed") {
h.writeError(w, "User already exists", http.StatusConflict) h.writeError(w, "User already exists", http.StatusConflict)
@@ -410,18 +472,15 @@ func (h *Handlers) CreateUser(w http.ResponseWriter, r *http.Request) {
return return
} }
if req.Approved {
if err := h.db.ApproveUser(user.ID); err != nil {
logrus.Errorf("Failed to approve user: %v", err)
}
}
h.writeJSON(w, user) h.writeJSON(w, user)
} }
// UpdateUser updates a user (admin endpoint) // UpdateUser updates a user (admin endpoint)
func (h *Handlers) UpdateUser(w http.ResponseWriter, r *http.Request) { func (h *Handlers) UpdateUser(w http.ResponseWriter, r *http.Request) {
// TODO: Add admin authentication check if !h.requireAdmin(w, r) {
return
}
vars := mux.Vars(r) vars := mux.Vars(r)
idStr, ok := vars["id"] idStr, ok := vars["id"]
if !ok { if !ok {
@@ -467,7 +526,10 @@ func (h *Handlers) UpdateUser(w http.ResponseWriter, r *http.Request) {
// DeleteUser deletes a user (admin endpoint) // DeleteUser deletes a user (admin endpoint)
func (h *Handlers) DeleteUser(w http.ResponseWriter, r *http.Request) { func (h *Handlers) DeleteUser(w http.ResponseWriter, r *http.Request) {
// TODO: Add admin authentication check if !h.requireAdmin(w, r) {
return
}
vars := mux.Vars(r) vars := mux.Vars(r)
idStr, ok := vars["id"] idStr, ok := vars["id"]
if !ok { if !ok {
@@ -489,3 +551,25 @@ func (h *Handlers) DeleteUser(w http.ResponseWriter, r *http.Request) {
h.writeJSON(w, map[string]string{"status": "success"}) h.writeJSON(w, map[string]string{"status": "success"})
} }
func (h *Handlers) isAdmin(r *http.Request) bool {
session, err := h.store.Get(r, "auth-session")
if err != nil {
return false
}
userEmail, ok := session.Values["user_email"].(string)
if !ok {
return false
}
return h.config.IsAdmin(userEmail)
}
func (h *Handlers) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
if !h.isAdmin(r) {
h.writeError(w, "Admin access required", http.StatusForbidden)
return false
}
return true
}
+1
View File
@@ -54,6 +54,7 @@ func main() {
api.HandleFunc("/login/begin", h.BeginLogin).Methods("POST") api.HandleFunc("/login/begin", h.BeginLogin).Methods("POST")
api.HandleFunc("/login/finish", h.FinishLogin).Methods("POST") api.HandleFunc("/login/finish", h.FinishLogin).Methods("POST")
api.HandleFunc("/logout", h.Logout).Methods("POST") api.HandleFunc("/logout", h.Logout).Methods("POST")
api.HandleFunc("/auth/status", h.GetAuthStatus).Methods("GET")
api.HandleFunc("/users", h.ListUsers).Methods("GET") api.HandleFunc("/users", h.ListUsers).Methods("GET")
api.HandleFunc("/users", h.CreateUser).Methods("POST") api.HandleFunc("/users", h.CreateUser).Methods("POST")
api.HandleFunc("/users/{id}", h.UpdateUser).Methods("PUT") api.HandleFunc("/users/{id}", h.UpdateUser).Methods("PUT")
+320 -150
View File
@@ -76,6 +76,41 @@
display: block; display: block;
} }
.auth-panel, .welcome-panel, .admin-panel {
margin-bottom: 2rem;
}
.auth-toggle {
text-align: center;
margin-top: 1rem;
}
.link-btn {
background: none;
border: none;
color: #667eea;
cursor: pointer;
text-decoration: underline;
font-size: 0.9rem;
}
.link-btn:hover {
color: #764ba2;
}
.welcome-panel {
text-align: center;
}
.welcome-actions {
margin-top: 1.5rem;
}
.admin-panel {
border-top: 1px solid #eee;
padding-top: 2rem;
}
.form-group { .form-group {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
@@ -207,46 +242,35 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>🔐 Passkey Auth</h1> <h1>🔐 Passkey Auth</h1>
<p>Admin Dashboard</p> <p id="headerSubtitle">Secure Authentication</p>
</div> </div>
<div class="tabs"> <!-- Initial Auth Panel -->
<button class="tab active" onclick="showTab('register')">Register User</button> <div id="authPanel" class="auth-panel">
<button class="tab" onclick="showTab('login')">Test Login</button> <h2 id="authTitle">Sign In</h2>
<button class="tab" onclick="showTab('users')">Manage Users</button> <form id="authForm">
</div>
<!-- Register Tab -->
<div id="register" class="tab-content active">
<h2>Register New User</h2>
<form id="registerForm">
<div class="form-group"> <div class="form-group">
<label for="email">Email Address:</label> <label for="email">Email Address:</label>
<input type="email" id="email" name="email" required> <input type="email" id="email" name="email" required>
</div> </div>
<div class="form-group"> <button type="submit" class="btn" id="authSubmitBtn">Sign In with Passkey</button>
<label for="displayName">Display Name:</label>
<input type="text" id="displayName" name="displayName" required>
</div>
<button type="submit" class="btn">Register with Passkey</button>
</form> </form>
<div class="auth-toggle">
<button type="button" class="link-btn" id="toggleModeBtn">Don't have an account? Sign up</button>
</div>
</div> </div>
<!-- Login Tab --> <!-- Welcome Panel (shown after auth) -->
<div id="login" class="tab-content"> <div id="welcomePanel" class="welcome-panel" style="display: none;">
<h2>Test Login</h2> <h2>Welcome!</h2>
<form id="loginForm"> <div id="welcomeMessage"></div>
<div class="form-group"> <div class="welcome-actions">
<label for="loginEmail">Email Address:</label> <button type="button" class="btn btn-danger" id="logoutBtn">Logout</button>
<input type="email" id="loginEmail" name="email" required> </div>
</div>
<button type="submit" class="btn">Login with Passkey</button>
</form>
<div id="loginStatus"></div>
</div> </div>
<!-- Users Tab --> <!-- Admin Panel (shown for admin users) -->
<div id="users" class="tab-content"> <div id="adminPanel" class="admin-panel" style="display: none;">
<h2>Manage Users</h2> <h2>Manage Users</h2>
<button class="btn" onclick="loadUsers()">Refresh Users</button> <button class="btn" onclick="loadUsers()">Refresh Users</button>
<div id="usersList" class="users-list"> <div id="usersList" class="users-list">
@@ -437,6 +461,10 @@
return response; return response;
} }
// UI State Management
let isSignUpMode = false;
let currentUser = null;
// Tab functionality // Tab functionality
function showTab(tabName) { function showTab(tabName) {
// Hide all tab contents // Hide all tab contents
@@ -461,6 +489,72 @@
} }
} }
// UI State Functions
function showAuthPanel() {
document.getElementById('authPanel').style.display = 'block';
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('adminPanel').style.display = 'none';
document.getElementById('headerSubtitle').textContent = 'Secure Authentication';
}
function showWelcomePanel(user) {
document.getElementById('authPanel').style.display = 'none';
document.getElementById('welcomePanel').style.display = 'block';
document.getElementById('headerSubtitle').textContent = 'Dashboard';
const welcomeMessage = document.getElementById('welcomeMessage');
welcomeMessage.innerHTML = `
<div class="alert alert-success">
<strong>Welcome back, ${user.display_name}!</strong><br>
<small>${user.email}</small>
</div>
`;
// Show admin panel if user is admin
if (user.is_admin) {
document.getElementById('adminPanel').style.display = 'block';
loadUsers();
} else {
document.getElementById('adminPanel').style.display = 'none';
}
}
function toggleSignUpMode() {
isSignUpMode = !isSignUpMode;
const authTitle = document.getElementById('authTitle');
const authSubmitBtn = document.getElementById('authSubmitBtn');
const toggleModeBtn = document.getElementById('toggleModeBtn');
if (isSignUpMode) {
authTitle.textContent = 'Sign Up';
authSubmitBtn.textContent = 'Sign Up with Passkey';
toggleModeBtn.textContent = 'Already have an account? Sign in';
} else {
authTitle.textContent = 'Sign In';
authSubmitBtn.textContent = 'Sign In with Passkey';
toggleModeBtn.textContent = "Don't have an account? Sign up";
}
}
function logout() {
fetch('/api/logout', {
method: 'POST',
credentials: 'include'
}).then(() => {
currentUser = null;
showAuthPanel();
showAlert('Logged out successfully');
// Clear email input
document.getElementById('email').value = '';
}).catch(error => {
showAlert('Logout failed: ' + error.message, 'error');
});
}
// Event Listeners
document.getElementById('toggleModeBtn').addEventListener('click', toggleSignUpMode);
document.getElementById('logoutBtn').addEventListener('click', logout);
// Alert functions // Alert functions
function showAlert(message, type = 'success') { function showAlert(message, type = 'success') {
const alertsContainer = document.getElementById('alerts'); const alertsContainer = document.getElementById('alerts');
@@ -472,147 +566,222 @@
setTimeout(() => { setTimeout(() => {
alert.remove(); alert.remove();
}, 5000); }, 5000);
} // Register functionality }
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const email = formData.get('email');
const displayName = formData.get('displayName');
// Authentication status management
async function checkAuthStatus() {
try { try {
console.log('Starting registration for email:', email); console.log('Checking authentication status...');
const response = await fetch('/api/auth/status', {
// Begin registration
const beginResponse = await fetch('/api/register/begin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
display_name: displayName
}),
credentials: 'include' credentials: 'include'
}); });
if (!beginResponse.ok) { console.log('Auth status response:', response.status, response.statusText);
throw new Error(await beginResponse.text());
if (response.ok) {
const userData = await response.json();
console.log('User is authenticated:', userData);
currentUser = userData.user;
showWelcomePanel(userData.user);
return userData;
} else {
console.log('User is not authenticated');
currentUser = null;
showAuthPanel();
return null;
} }
const options = await beginResponse.json();
console.log('Raw options from server:', JSON.stringify(options, null, 2));
// Convert base64url encoded fields to ArrayBuffers
const webAuthnOptions = prepareWebAuthnOptions(options);
console.log('Final webAuthnOptions for navigator.credentials.create:', webAuthnOptions);
// Create credential
const credential = await navigator.credentials.create(webAuthnOptions);
console.log('Raw credential from navigator.credentials.create:', credential);
// Convert ArrayBuffers back to base64url for JSON
const credentialResponse = prepareWebAuthnResponse(credential);
console.log('Prepared credential response to send to server:', JSON.stringify(credentialResponse, null, 2));
// Finish registration
const finishResponse = await fetch('/api/register/finish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentialResponse),
credentials: 'include'
});
console.log('Finish registration response status:', finishResponse.status);
if (!finishResponse.ok) {
const errorText = await finishResponse.text();
console.error('Finish registration error:', errorText);
throw new Error(errorText);
}
showAlert('User registered successfully!');
e.target.reset();
} catch (error) { } catch (error) {
showAlert(`Registration failed: ${error.message}`, 'error'); console.log('Auth status check failed:', error);
currentUser = null;
showAuthPanel();
return null;
} }
}); // Login functionality }
document.getElementById('loginForm').addEventListener('submit', async (e) => {
function showUsersTab() {
console.log('showUsersTab called');
const usersTab = document.getElementById('usersTab');
if (usersTab) {
usersTab.style.display = 'block';
console.log('Users tab is now visible');
} else {
console.log('Could not find usersTab element');
}
}
function hideUsersTab() {
console.log('hideUsersTab called');
const usersTab = document.getElementById('usersTab');
const usersContent = document.getElementById('users');
if (usersTab) {
usersTab.style.display = 'none';
console.log('Users tab is now hidden');
}
// If users tab is currently active, switch to register tab
if (usersContent && usersContent.classList.contains('active')) {
console.log('Users tab was active, switching to register tab');
showTab('register');
}
} // Unified Auth Form Handler
document.getElementById('authForm').addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.target); const email = document.getElementById('email').value;
const email = formData.get('email'); const submitBtn = document.getElementById('authSubmitBtn');
// Disable submit button during processing
submitBtn.disabled = true;
const originalText = submitBtn.textContent;
submitBtn.textContent = isSignUpMode ? 'Signing up...' : 'Signing in...';
try { try {
// Begin login if (isSignUpMode) {
const beginResponse = await fetch('/api/login/begin', { await handleSignUp(email);
method: 'POST', } else {
headers: { await handleSignIn(email);
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email
}),
credentials: 'include'
});
if (!beginResponse.ok) {
throw new Error(await beginResponse.text());
} }
const options = await beginResponse.json();
console.log('Raw login options from server:', JSON.stringify(options, null, 2));
// Convert base64url encoded fields to ArrayBuffers
const webAuthnOptions = prepareWebAuthnOptions(options);
console.log('Final login webAuthnOptions for navigator.credentials.get:', webAuthnOptions);
// Get credential
const credential = await navigator.credentials.get(webAuthnOptions);
// Convert ArrayBuffers back to base64url for JSON
const credentialResponse = prepareWebAuthnResponse(credential);
// Finish login
const finishResponse = await fetch('/api/login/finish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentialResponse),
credentials: 'include'
});
if (!finishResponse.ok) {
throw new Error(await finishResponse.text());
}
const result = await finishResponse.json();
showAlert(`Login successful! Welcome, ${result.user.display_name}`);
document.getElementById('loginStatus').innerHTML = `
<div class="alert alert-success">
<strong>Logged in as:</strong> ${result.user.display_name} (${result.user.email})
</div>
`;
} catch (error) { } catch (error) {
showAlert(`Login failed: ${error.message}`, 'error'); showAlert(`${isSignUpMode ? 'Sign up' : 'Sign in'} failed: ${error.message}`, 'error');
document.getElementById('loginStatus').innerHTML = ` } finally {
<div class="alert alert-error"> // Re-enable submit button
Login failed: ${error.message} submitBtn.disabled = false;
</div> submitBtn.textContent = originalText;
`;
} }
}); });
// Sign Up Handler
async function handleSignUp(email) {
console.log('Starting registration for email:', email);
// Use email as display name (extract name part before @)
const displayName = email.split('@')[0];
// Begin registration
const beginResponse = await fetch('/api/register/begin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
display_name: displayName
}),
credentials: 'include'
});
if (!beginResponse.ok) {
throw new Error(await beginResponse.text());
}
const options = await beginResponse.json();
console.log('Raw options from server:', JSON.stringify(options, null, 2));
// Convert base64url encoded fields to ArrayBuffers
const webAuthnOptions = prepareWebAuthnOptions(options);
console.log('Final webAuthnOptions for navigator.credentials.create:', webAuthnOptions);
// Create credential
const credential = await navigator.credentials.create(webAuthnOptions);
console.log('Raw credential from navigator.credentials.create:', credential);
// Convert ArrayBuffers back to base64url for JSON
const credentialResponse = prepareWebAuthnResponse(credential);
console.log('Prepared credential response to send to server:', JSON.stringify(credentialResponse, null, 2));
// Finish registration
const finishResponse = await fetch('/api/register/finish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentialResponse),
credentials: 'include'
});
console.log('Finish registration response status:', finishResponse.status);
if (!finishResponse.ok) {
const errorText = await finishResponse.text();
console.error('Finish registration error:', errorText);
throw new Error(errorText);
}
showAlert('Account created successfully!');
// Check auth status to show welcome panel
setTimeout(checkAuthStatus, 100);
}
// Sign In Handler
async function handleSignIn(email) {
// Begin login
const beginResponse = await fetch('/api/login/begin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email
}),
credentials: 'include'
});
if (!beginResponse.ok) {
throw new Error(await beginResponse.text());
}
const options = await beginResponse.json();
console.log('Raw login options from server:', JSON.stringify(options, null, 2));
// Convert base64url encoded fields to ArrayBuffers
const webAuthnOptions = prepareWebAuthnOptions(options);
console.log('Final login webAuthnOptions for navigator.credentials.get:', webAuthnOptions);
// Get credential
const credential = await navigator.credentials.get(webAuthnOptions);
// Convert ArrayBuffers back to base64url for JSON
const credentialResponse = prepareWebAuthnResponse(credential);
// Finish login
const finishResponse = await fetch('/api/login/finish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentialResponse),
credentials: 'include'
});
if (!finishResponse.ok) {
throw new Error(await finishResponse.text());
}
const result = await finishResponse.json();
showAlert(`Welcome back, ${result.user.display_name}!`);
// Check auth status to show welcome panel
setTimeout(checkAuthStatus, 100);
}
// Users management // Users management
async function loadUsers() { async function loadUsers() {
const usersList = document.getElementById('usersList'); const usersList = document.getElementById('usersList');
usersList.innerHTML = '<div class="loading">Loading users...</div>'; usersList.innerHTML = '<div class="loading">Loading users...</div>';
try { try {
const response = await fetch('/api/users'); const response = await fetch('/api/users', {
credentials: 'include'
});
if (response.status === 403) {
// User is not admin, hide the tab
hideUsersTab();
showAlert('Access denied: Admin privileges required', 'error');
return;
}
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to load users'); throw new Error('Failed to load users');
} }
@@ -692,9 +861,10 @@
} }
} }
// Load users when page loads // Initialize page
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
loadUsers(); // Check authentication status to determine initial UI state
await checkAuthStatus();
}); });
</script> </script>
</body> </body>