mirror of
https://github.com/wahyd4/passkey-auth.git
synced 2026-08-08 20:15:44 +10:00
Merge pull request #7 from wahyd4/wildcard-domain
Support wildcard domain
This commit is contained in:
@@ -70,6 +70,8 @@ webauthn:
|
||||
cors:
|
||||
allowed_origins:
|
||||
- "https://your-domain.com" # Your domain with protocol
|
||||
# Wildcard domains are supported for subdomains:
|
||||
- "*.your-domain.com" # Matches api.your-domain.com, app.your-domain.com, etc.
|
||||
|
||||
auth:
|
||||
session_secret: "your-secure-secret-key" # Generate a secure random string
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"passkey-auth/internal/cors"
|
||||
)
|
||||
|
||||
func TestWildcardCORSIntegration(t *testing.T) {
|
||||
// Create a simple test handler
|
||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
// Create CORS middleware with wildcard support
|
||||
corsMiddleware := cors.WildcardCORS(cors.Config{
|
||||
AllowedOrigins: []string{"*.junv.cc", "https://static.example.com"},
|
||||
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||
AllowedHeaders: []string{"*"},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
|
||||
// Wrap the test handler
|
||||
handler := corsMiddleware(testHandler)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
origin string
|
||||
expectAllowed bool
|
||||
expectedOrigin string
|
||||
}{
|
||||
{
|
||||
name: "wildcard subdomain match",
|
||||
origin: "https://api.junv.cc",
|
||||
expectAllowed: true,
|
||||
expectedOrigin: "https://api.junv.cc",
|
||||
},
|
||||
{
|
||||
name: "wildcard base domain match",
|
||||
origin: "https://junv.cc",
|
||||
expectAllowed: true,
|
||||
expectedOrigin: "https://junv.cc",
|
||||
},
|
||||
{
|
||||
name: "static domain match",
|
||||
origin: "https://static.example.com",
|
||||
expectAllowed: true,
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
origin: "https://evil.com",
|
||||
expectAllowed: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a preflight OPTIONS request
|
||||
req := httptest.NewRequest("OPTIONS", "/", nil)
|
||||
req.Header.Set("Origin", tt.origin)
|
||||
req.Header.Set("Access-Control-Request-Method", "POST")
|
||||
|
||||
// Record the response
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
// Check CORS headers
|
||||
allowOriginHeader := w.Header().Get("Access-Control-Allow-Origin")
|
||||
|
||||
if tt.expectAllowed {
|
||||
if allowOriginHeader == "" {
|
||||
t.Errorf("Expected Access-Control-Allow-Origin header, but got none")
|
||||
}
|
||||
|
||||
// For wildcard matches, should return the specific origin
|
||||
if tt.expectedOrigin != "" && allowOriginHeader != tt.expectedOrigin {
|
||||
t.Errorf("Expected Access-Control-Allow-Origin: %s, got: %s", tt.expectedOrigin, allowOriginHeader)
|
||||
}
|
||||
} else {
|
||||
if allowOriginHeader != "" {
|
||||
t.Errorf("Expected no Access-Control-Allow-Origin header, but got: %s", allowOriginHeader)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
// Config holds the CORS configuration with wildcard support
|
||||
type Config struct {
|
||||
AllowedOrigins []string
|
||||
AllowedMethods []string
|
||||
AllowedHeaders []string
|
||||
AllowCredentials bool
|
||||
}
|
||||
|
||||
// WildcardCORS creates a CORS handler with wildcard domain support
|
||||
func WildcardCORS(config Config) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
// Determine allowed origins for this request
|
||||
var allowedOrigins []string
|
||||
|
||||
// Separate wildcard and static origins
|
||||
var wildcardPatterns []string
|
||||
var staticOrigins []string
|
||||
|
||||
for _, configuredOrigin := range config.AllowedOrigins {
|
||||
if strings.Contains(configuredOrigin, "*") {
|
||||
wildcardPatterns = append(wildcardPatterns, configuredOrigin)
|
||||
} else {
|
||||
staticOrigins = append(staticOrigins, configuredOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if origin matches any wildcard pattern
|
||||
if len(wildcardPatterns) > 0 && origin != "" {
|
||||
wildcardMatcher := NewWildcardMatcher(wildcardPatterns)
|
||||
if wildcardMatcher.MatchOrigin(origin) {
|
||||
// For wildcard matches, allow the specific origin
|
||||
allowedOrigins = []string{origin}
|
||||
}
|
||||
}
|
||||
|
||||
// If no wildcard match, use static origins
|
||||
if len(allowedOrigins) == 0 {
|
||||
allowedOrigins = staticOrigins
|
||||
} else {
|
||||
// If we had a wildcard match, also include static origins
|
||||
allowedOrigins = append(allowedOrigins, staticOrigins...)
|
||||
}
|
||||
|
||||
// Create a new CORS instance for this request with the determined origins
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: allowedOrigins,
|
||||
AllowedMethods: config.AllowedMethods,
|
||||
AllowedHeaders: config.AllowedHeaders,
|
||||
AllowCredentials: config.AllowCredentials,
|
||||
})
|
||||
|
||||
// Use the rs/cors handler
|
||||
c.Handler(next).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WildcardMatcher provides wildcard domain matching for CORS origins
|
||||
type WildcardMatcher struct {
|
||||
patterns []string
|
||||
}
|
||||
|
||||
// NewWildcardMatcher creates a new wildcard matcher with the given patterns
|
||||
func NewWildcardMatcher(patterns []string) *WildcardMatcher {
|
||||
return &WildcardMatcher{
|
||||
patterns: patterns,
|
||||
}
|
||||
}
|
||||
|
||||
// MatchOrigin checks if the given origin matches any of the wildcard patterns
|
||||
func (m *WildcardMatcher) MatchOrigin(origin string) bool {
|
||||
for _, pattern := range m.patterns {
|
||||
if m.matchPattern(origin, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchPattern checks if origin matches a specific pattern
|
||||
// Supports patterns like:
|
||||
// - "*.example.com" matches "api.example.com", "auth.example.com", etc.
|
||||
// - "*.*.example.com" matches "api.v1.example.com", etc.
|
||||
// - "example.com" matches exactly "example.com"
|
||||
func (m *WildcardMatcher) matchPattern(origin, pattern string) bool {
|
||||
// Remove protocol from origin if present
|
||||
origin = strings.TrimPrefix(origin, "https://")
|
||||
origin = strings.TrimPrefix(origin, "http://")
|
||||
|
||||
// Remove port if present
|
||||
if colonIndex := strings.LastIndex(origin, ":"); colonIndex != -1 && colonIndex > strings.LastIndex(origin, "]") {
|
||||
origin = origin[:colonIndex]
|
||||
}
|
||||
|
||||
// Exact match
|
||||
if origin == pattern {
|
||||
return true
|
||||
}
|
||||
|
||||
// Wildcard match
|
||||
if strings.Contains(pattern, "*") {
|
||||
return m.wildcardMatch(origin, pattern)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// wildcardMatch performs wildcard matching
|
||||
func (m *WildcardMatcher) wildcardMatch(origin, pattern string) bool {
|
||||
// Handle simple case: *.domain.com
|
||||
if strings.HasPrefix(pattern, "*.") {
|
||||
suffix := pattern[2:] // Remove "*."
|
||||
|
||||
// Check if origin ends with the suffix and has at least one subdomain
|
||||
if strings.HasSuffix(origin, "."+suffix) {
|
||||
// Ensure there's a subdomain (not just the suffix itself)
|
||||
prefix := strings.TrimSuffix(origin, "."+suffix)
|
||||
// Make sure the prefix doesn't contain dots (single-level subdomain wildcard)
|
||||
// If you want multi-level subdomains, remove this check
|
||||
return !strings.Contains(prefix, ".")
|
||||
}
|
||||
|
||||
// Also check if origin exactly matches the suffix (without subdomain)
|
||||
return origin == suffix
|
||||
}
|
||||
|
||||
// For more complex patterns, we could implement more sophisticated matching
|
||||
// For now, handle the common *.domain.com case
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAllowedOrigins returns the actual allowed origins for a request
|
||||
// This expands wildcard patterns based on the request origin
|
||||
func (m *WildcardMatcher) GetAllowedOrigins(requestOrigin string, staticOrigins []string) []string {
|
||||
allowedOrigins := make([]string, 0, len(staticOrigins))
|
||||
|
||||
for _, origin := range staticOrigins {
|
||||
if strings.Contains(origin, "*") {
|
||||
// This is a wildcard pattern
|
||||
if m.matchPattern(requestOrigin, origin) {
|
||||
// Add the actual request origin instead of the pattern
|
||||
allowedOrigins = append(allowedOrigins, requestOrigin)
|
||||
}
|
||||
} else {
|
||||
// This is a static origin, add as-is
|
||||
allowedOrigins = append(allowedOrigins, origin)
|
||||
}
|
||||
}
|
||||
|
||||
return allowedOrigins
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWildcardMatcher(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
origin string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "exact match",
|
||||
patterns: []string{"example.com"},
|
||||
origin: "example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "exact match with https",
|
||||
patterns: []string{"example.com"},
|
||||
origin: "https://example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard subdomain match",
|
||||
patterns: []string{"*.junv.cc"},
|
||||
origin: "api.junv.cc",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard subdomain match with https",
|
||||
patterns: []string{"*.junv.cc"},
|
||||
origin: "https://auth.junv.cc",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard subdomain match with port",
|
||||
patterns: []string{"*.junv.cc"},
|
||||
origin: "https://dev.junv.cc:3000",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard base domain match",
|
||||
patterns: []string{"*.junv.cc"},
|
||||
origin: "junv.cc",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard no match - different domain",
|
||||
patterns: []string{"*.junv.cc"},
|
||||
origin: "api.example.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard no match - multi-level subdomain",
|
||||
patterns: []string{"*.junv.cc"},
|
||||
origin: "api.v1.junv.cc",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "multiple patterns - first match",
|
||||
patterns: []string{"*.junv.cc", "*.example.com"},
|
||||
origin: "api.junv.cc",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "multiple patterns - second match",
|
||||
patterns: []string{"*.junv.cc", "*.example.com"},
|
||||
origin: "api.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
patterns: []string{"*.junv.cc", "*.example.com"},
|
||||
origin: "api.other.com",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
matcher := NewWildcardMatcher(tt.patterns)
|
||||
result := matcher.MatchOrigin(tt.origin)
|
||||
if result != tt.expected {
|
||||
t.Errorf("MatchOrigin() = %v, expected %v for origin %s with patterns %v",
|
||||
result, tt.expected, tt.origin, tt.patterns)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllowedOrigins(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
requestOrigin string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "wildcard match includes request origin",
|
||||
patterns: []string{"*.junv.cc", "https://static.com"},
|
||||
requestOrigin: "https://api.junv.cc",
|
||||
expected: []string{"https://api.junv.cc", "https://static.com"},
|
||||
},
|
||||
{
|
||||
name: "no wildcard match returns static origins",
|
||||
patterns: []string{"*.junv.cc", "https://static.com"},
|
||||
requestOrigin: "https://other.com",
|
||||
expected: []string{"*.junv.cc", "https://static.com"},
|
||||
},
|
||||
{
|
||||
name: "multiple wildcards, one matches",
|
||||
patterns: []string{"*.junv.cc", "*.example.com"},
|
||||
requestOrigin: "https://api.junv.cc",
|
||||
expected: []string{"https://api.junv.cc"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
matcher := NewWildcardMatcher(tt.patterns)
|
||||
result := matcher.GetAllowedOrigins(tt.requestOrigin, tt.patterns)
|
||||
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("GetAllowedOrigins() returned %d origins, expected %d", len(result), len(tt.expected))
|
||||
return
|
||||
}
|
||||
|
||||
for i, expected := range tt.expected {
|
||||
if result[i] != expected {
|
||||
t.Errorf("GetAllowedOrigins()[%d] = %v, expected %v", i, result[i], expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: passkey-auth-config
|
||||
namespace: passkey-auth
|
||||
data:
|
||||
config.yaml: |
|
||||
server:
|
||||
port: "8080"
|
||||
host: "0.0.0.0"
|
||||
|
||||
webauthn:
|
||||
rp_display_name: "Passkey Auth"
|
||||
rp_id: "your-domain.com"
|
||||
rp_origins:
|
||||
- "https://your-domain.com"
|
||||
|
||||
database:
|
||||
path: "/data/passkey-auth.db"
|
||||
|
||||
cors:
|
||||
allowed_origins:
|
||||
- "https://your-domain.com"
|
||||
|
||||
auth:
|
||||
session_secret: "your-session-secret-change-me"
|
||||
require_approval: true
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: passkey-auth-storage
|
||||
namespace: passkey-auth
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: passkey-auth
|
||||
namespace: passkey-auth
|
||||
labels:
|
||||
app: passkey-auth
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: passkey-auth
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: passkey-auth
|
||||
spec:
|
||||
containers:
|
||||
- name: passkey-auth
|
||||
image: passkey-auth:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: CONFIG_PATH
|
||||
value: "/config/config.yaml"
|
||||
- name: DATABASE_PATH
|
||||
value: "/data/passkey-auth.db"
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
readOnly: true
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: passkey-auth-config
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: passkey-auth-storage
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: passkey-auth-service
|
||||
namespace: passkey-auth
|
||||
labels:
|
||||
app: passkey-auth
|
||||
spec:
|
||||
selector:
|
||||
app: passkey-auth
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
type: ClusterIP
|
||||
@@ -1,44 +0,0 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: example-app-ingress
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: "http://passkey-auth-service.passkey-auth.svc.cluster.local/auth"
|
||||
nginx.ingress.kubernetes.io/auth-signin: "https://your-domain.com/auth"
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "X-Auth-User,X-Auth-User-ID"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: your-app.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: your-app-service
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: passkey-auth-ingress
|
||||
namespace: passkey-auth
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: your-domain.com
|
||||
http:
|
||||
paths:
|
||||
- path: /auth
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: passkey-auth-service
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: passkey-auth
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/cors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"passkey-auth/internal/auth"
|
||||
"passkey-auth/internal/config"
|
||||
"passkey-auth/internal/cors"
|
||||
"passkey-auth/internal/database"
|
||||
"passkey-auth/internal/handlers"
|
||||
)
|
||||
@@ -74,15 +74,15 @@ func main() {
|
||||
// Static files for admin UI
|
||||
router.PathPrefix("/").Handler(http.FileServer(http.Dir("./web/"))).Methods("GET")
|
||||
|
||||
// Setup CORS
|
||||
c := cors.New(cors.Options{
|
||||
// Setup CORS with wildcard support
|
||||
corsHandler := cors.WildcardCORS(cors.Config{
|
||||
AllowedOrigins: cfg.CORS.AllowedOrigins,
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"*"},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
|
||||
handler := c.Handler(router)
|
||||
handler := corsHandler(router)
|
||||
|
||||
// Start server
|
||||
port := os.Getenv("PORT")
|
||||
|
||||
+25
-25
@@ -76,7 +76,7 @@
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
@@ -117,18 +117,18 @@
|
||||
<h2 id="authTitle">Sign in to your account</h2>
|
||||
<p>Use your passkey for secure authentication</p>
|
||||
</header>
|
||||
|
||||
|
||||
<form id="authForm">
|
||||
<label for="email">
|
||||
Email address
|
||||
<input type="email" id="email" name="email" placeholder="Enter your email address" required>
|
||||
</label>
|
||||
|
||||
|
||||
<button type="submit" id="authSubmitBtn">
|
||||
Sign in with passkey
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
<footer class="auth-toggle">
|
||||
<button type="button" class="link-btn" id="toggleModeBtn">
|
||||
Don't have an account? Create one
|
||||
@@ -144,9 +144,9 @@
|
||||
<h2>Welcome back!</h2>
|
||||
<p>You're successfully authenticated</p>
|
||||
</header>
|
||||
|
||||
|
||||
<div id="welcomeMessage"></div>
|
||||
|
||||
|
||||
<footer>
|
||||
<button type="button" class="secondary" id="logoutBtn">
|
||||
Sign out
|
||||
@@ -162,11 +162,11 @@
|
||||
<h3>User Management</h3>
|
||||
<p>Manage user accounts and permissions</p>
|
||||
</header>
|
||||
|
||||
|
||||
<button onclick="loadUsers()" class="outline">
|
||||
Refresh users
|
||||
</button>
|
||||
|
||||
|
||||
<div id="usersList" class="users-list">
|
||||
<article aria-busy="true">Loading users...</article>
|
||||
</div>
|
||||
@@ -409,11 +409,11 @@
|
||||
document.getElementById('authPanel').style.display = 'none';
|
||||
document.getElementById('welcomePanel').style.display = 'block';
|
||||
document.getElementById('headerSubtitle').textContent = 'Dashboard';
|
||||
|
||||
|
||||
const welcomeMessage = document.getElementById('welcomeMessage');
|
||||
welcomeMessage.innerHTML = `
|
||||
<p>Hello, <strong>${user.display_name}</strong>! You're successfully authenticated and ready to go.</p>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Account Information</summary>
|
||||
<table>
|
||||
@@ -487,7 +487,7 @@
|
||||
function showAlert(message, type = 'success') {
|
||||
const alertsContainer = document.getElementById('alerts');
|
||||
const alert = document.createElement('article');
|
||||
|
||||
|
||||
// Use appropriate styling based on type
|
||||
if (type === 'success') {
|
||||
alert.style.borderLeftColor = 'var(--pico-ins-color)';
|
||||
@@ -500,7 +500,7 @@
|
||||
alert.style.borderLeftStyle = 'solid';
|
||||
alert.innerHTML = `<strong>Error:</strong> ${message}`;
|
||||
}
|
||||
|
||||
|
||||
alertsContainer.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -553,12 +553,12 @@
|
||||
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');
|
||||
@@ -570,13 +570,13 @@
|
||||
|
||||
const email = document.getElementById('email').value;
|
||||
const submitBtn = document.getElementById('authSubmitBtn');
|
||||
|
||||
|
||||
// Disable submit button during processing
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.setAttribute('aria-busy', 'true');
|
||||
const originalText = submitBtn.textContent;
|
||||
submitBtn.textContent = isSignUpMode ?
|
||||
'Creating account...' :
|
||||
submitBtn.textContent = isSignUpMode ?
|
||||
'Creating account...' :
|
||||
'Signing in...';
|
||||
|
||||
try {
|
||||
@@ -652,12 +652,12 @@
|
||||
}
|
||||
|
||||
showAlert('Account created successfully! Welcome to the platform.');
|
||||
|
||||
|
||||
// Check auth status to show welcome panel
|
||||
setTimeout(checkAuthStatus, 100);
|
||||
}
|
||||
|
||||
// Sign In Handler
|
||||
// Sign In Handler
|
||||
async function handleSignIn(email) {
|
||||
// Begin login
|
||||
const beginResponse = await fetch('/api/login/begin', {
|
||||
@@ -704,7 +704,7 @@
|
||||
|
||||
const result = await finishResponse.json();
|
||||
showAlert(`Welcome back, ${result.user.display_name}!`);
|
||||
|
||||
|
||||
// Check auth status to show welcome panel
|
||||
setTimeout(checkAuthStatus, 100);
|
||||
}
|
||||
@@ -718,14 +718,14 @@
|
||||
const response = await fetch('/api/users', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.status === 403) {
|
||||
// User is not admin, hide the tab
|
||||
hideUsersTab();
|
||||
showAlert('Access denied: Administrator privileges required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load users');
|
||||
}
|
||||
@@ -743,9 +743,9 @@
|
||||
<h4>${user.display_name}</h4>
|
||||
<p>${user.email}</p>
|
||||
</header>
|
||||
|
||||
|
||||
<p><small>Created: ${new Date(user.created_at).toLocaleDateString()}</small></p>
|
||||
|
||||
|
||||
<footer class="user-actions">
|
||||
<span class="status-badge ${user.approved ? 'status-approved' : 'status-pending'}">
|
||||
${user.approved ? 'Approved' : 'Pending Approval'}
|
||||
@@ -814,7 +814,7 @@
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Load theme first
|
||||
loadTheme();
|
||||
|
||||
|
||||
// Check authentication status to determine initial UI state
|
||||
await checkAuthStatus();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user