summaryrefslogtreecommitdiff
path: root/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'handlers')
-rw-r--r--handlers/admin.go155
-rw-r--r--handlers/auth.go221
-rw-r--r--handlers/proxy.go38
-rw-r--r--handlers/user.go100
4 files changed, 514 insertions, 0 deletions
diff --git a/handlers/admin.go b/handlers/admin.go
new file mode 100644
index 0000000..d9fb310
--- /dev/null
+++ b/handlers/admin.go
@@ -0,0 +1,155 @@
+package handlers
+
+import (
+ "auth-proxy/config"
+ "auth-proxy/logging"
+ "auth-proxy/models"
+ "auth-proxy/types"
+ "encoding/json"
+ "net/http"
+ "strconv"
+)
+
+// AdminPageHandler serves the admin dashboard page.
+func AdminPageHandler(w http.ResponseWriter, r *http.Request) {
+ // Fetch all users to render on the page
+ users, err := models.GetAllUsers()
+ if err != nil {
+ logging.AppLog.Error("Failed to retrieve users for admin page", "error", err)
+ http.Error(w, "Could not load user data.", http.StatusInternalServerError)
+ return
+ }
+
+ data := map[string]interface{}{
+ "Paths": config.Paths,
+ "Users": users,
+ }
+ templates.ExecuteTemplate(w, "admin.html", data)
+}
+
+// GetUsersHandler returns a list of all users.
+func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ users, err := models.GetAllUsers()
+ if err != nil {
+ logging.AppLog.Error("Failed to retrieve users", "error", err, "ip", ip)
+ http.Error(w, "Failed to retrieve users", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(users)
+}
+
+// UpdateUserRoleHandler updates a user's role.
+func UpdateUserRoleHandler(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ // Get admin user from context
+ adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*models.User)
+ if !ok || adminUser == nil {
+ logging.AppLog.Error("Failed to get user from context for admin action", "ip", ip)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ adminUsername := adminUser.Username
+
+ id, err := strconv.ParseInt(r.URL.Path[len(config.Paths.AdminUsersAPI+"/"):len(r.URL.Path)-len("/role")], 10, 64)
+ if err != nil {
+ logging.AppLog.Warn("Invalid user ID in request", "path", r.URL.Path, "ip", ip)
+ http.Error(w, "Invalid user ID", http.StatusBadRequest)
+ return
+ }
+
+ var req struct {
+ Role string `json:"role"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ logging.AppLog.Warn("Invalid request body for role update", "error", err, "ip", ip)
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ if req.Role != "admin" && req.Role != "user" {
+ logging.AppLog.Warn("Invalid role provided for update", "role", req.Role, "ip", ip)
+ http.Error(w, "Invalid role", http.StatusBadRequest)
+ return
+ }
+
+ if err := models.UpdateUserRole(id, req.Role); err != nil {
+ logging.AppLog.Error("Failed to update user role in database", "error", err, "target_user_id", id, "ip", ip)
+ http.Error(w, "Failed to update role", http.StatusInternalServerError)
+ return
+ }
+
+ logging.SecurityLog.Info("ADMIN ACTION", "action", "update_role", "performed_by", adminUsername, "target_user_id", id, "new_role", req.Role, "ip", ip)
+ w.WriteHeader(http.StatusOK)
+}
+
+// DeleteUserHandler deletes a user.
+func DeleteUserHandler(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ // Get admin user from context
+ adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*models.User)
+ if !ok || adminUser == nil {
+ logging.AppLog.Error("Failed to get user from context for admin action", "ip", ip)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ adminUsername := adminUser.Username
+
+ id, err := strconv.ParseInt(r.URL.Path[len(config.Paths.AdminUsersAPI+"/"):], 10, 64)
+ if err != nil {
+ logging.AppLog.Warn("Invalid user ID in request", "path", r.URL.Path, "ip", ip)
+ http.Error(w, "Invalid user ID", http.StatusBadRequest)
+ return
+ }
+
+ if err := models.DeleteUser(id); err != nil {
+ logging.AppLog.Error("Failed to delete user from database", "error", err, "target_user_id", id, "ip", ip)
+ http.Error(w, "Failed to delete user", http.StatusInternalServerError)
+ return
+ }
+
+ logging.SecurityLog.Info("ADMIN ACTION", "action", "delete_user", "performed_by", adminUsername, "target_user_id", id, "ip", ip)
+ w.WriteHeader(http.StatusOK)
+}
+
+// SetUserActiveStatusHandler updates a user's active status.
+func SetUserActiveStatusHandler(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ // Get admin user from context
+ adminUser, ok := r.Context().Value(types.SessionDataContextKey).(*models.User)
+ if !ok || adminUser == nil {
+ logging.AppLog.Error("Failed to get user from context for admin action", "ip", ip)
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+ adminUsername := adminUser.Username
+
+ id, err := strconv.ParseInt(r.URL.Path[len(config.Paths.AdminUsersAPI+"/"):len(r.URL.Path)-len("/status")], 10, 64)
+ if err != nil {
+ logging.AppLog.Warn("Invalid user ID in request", "path", r.URL.Path, "ip", ip)
+ http.Error(w, "Invalid user ID", http.StatusBadRequest)
+ return
+ }
+
+ var req struct {
+ IsActive bool `json:"is_active"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ logging.AppLog.Warn("Invalid request body for status update", "error", err, "ip", ip)
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ if err := models.SetUserActiveStatus(id, req.IsActive); err != nil {
+ logging.AppLog.Error("Failed to update user status in database", "error", err, "target_user_id", id, "ip", ip)
+ http.Error(w, "Failed to update status", http.StatusInternalServerError)
+ return
+ }
+
+ logging.SecurityLog.Info("ADMIN ACTION", "action", "set_active_status", "performed_by", adminUsername, "target_user_id", id, "new_status", req.IsActive, "ip", ip)
+ w.WriteHeader(http.StatusOK)
+} \ No newline at end of file
diff --git a/handlers/auth.go b/handlers/auth.go
new file mode 100644
index 0000000..28c7fa7
--- /dev/null
+++ b/handlers/auth.go
@@ -0,0 +1,221 @@
+package handlers
+
+import (
+ "auth-proxy/config"
+ "auth-proxy/logging"
+ "auth-proxy/models"
+ "auth-proxy/utils"
+ "encoding/json"
+ "html/template"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+)
+
+var templates = template.Must(template.ParseGlob("templates/*.html"))
+
+func renderLoginWithError(w http.ResponseWriter, message string) {
+ data := map[string]interface{}{
+ "Paths": config.Paths,
+ "Error": message,
+ }
+ w.WriteHeader(http.StatusUnauthorized) // Set status to indicate failure
+ templates.ExecuteTemplate(w, "login.html", data)
+}
+
+// LoginHandler handles displaying the login form and processing login requests.
+func LoginHandler(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ data := map[string]interface{}{
+ "Paths": config.Paths,
+ }
+
+ if r.Method == http.MethodGet {
+ templates.ExecuteTemplate(w, "login.html", data)
+ return
+ }
+
+ if r.Method == http.MethodPost {
+ r.ParseForm()
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+
+ user, err := models.GetUserByUsername(username)
+ if err != nil {
+ logging.AppLog.Error("Database error during login", "error", err, "username", username, "ip", ip)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": "An internal error occurred."})
+ return
+ }
+
+ // Check for lockout and user status first
+ if user != nil {
+ lockoutMinutes, _ := strconv.Atoi(os.Getenv("LOCKOUT_DURATION_MINUTES"))
+
+ if !user.IsActive {
+ lockoutDuration := time.Duration(lockoutMinutes) * time.Minute
+ if time.Since(user.UpdatedAt) < lockoutDuration {
+ logging.SecurityLog.Warn("LOGIN FAIL", "username", username, "ip", ip, "reason", "account locked")
+ renderLoginWithError(w, "Account is temporarily locked.")
+ return
+ } else {
+ // If lockout expired, re-activate account and reset attempts
+ models.SetUserActiveStatus(user.ID, true)
+ models.RecordLoginSuccess(user.ID) // Resets failed_logins
+ }
+ }
+ }
+
+ // Validate user credentials
+ if user == nil || !models.CheckPasswordHash(password, user.PasswordHash) {
+ if user != nil {
+ models.RecordLoginFailure(user.ID)
+ // Re-fetch user to get updated failed_logins count
+ updatedUser, _ := models.GetUserByID(user.ID)
+ maxAttempts, _ := strconv.Atoi(os.Getenv("MAX_LOGIN_ATTEMPTS"))
+ if updatedUser.FailedLogins >= maxAttempts {
+ models.SetUserActiveStatus(user.ID, false)
+ logging.SecurityLog.Warn("ACCOUNT LOCKED", "username", username, "ip", ip)
+ }
+ }
+ logging.SecurityLog.Warn("LOGIN FAIL", "username", username, "ip", ip, "reason", "invalid credentials")
+ renderLoginWithError(w, "Invalid username or password.")
+ return
+ }
+
+ // Login successful
+ models.RecordLoginSuccess(user.ID)
+ logging.SecurityLog.Info("LOGIN SUCCESS", "username", user.Username, "ip", ip)
+
+ tokenDurationHours, err := strconv.Atoi(os.Getenv("TOKEN_DURATION_HOURS"))
+ if err != nil || tokenDurationHours <= 0 {
+ tokenDurationHours = 24 // Default to 24 hours
+ }
+ duration := time.Duration(tokenDurationHours) * time.Hour
+
+ token, err := models.CreateAuthToken(user.ID, duration)
+ if err != nil {
+ logging.AppLog.Error("Failed to create auth token", "error", err, "username", user.Username, "ip", ip)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Could not create session."})
+ return
+ }
+
+ http.SetCookie(w, &http.Cookie{
+ Name: "auth_token",
+ Value: token.Token,
+ Expires: token.ExpiresAt,
+ Path: "/",
+ HttpOnly: true,
+ Secure: r.TLS != nil, // Use Secure flag if on HTTPS
+ SameSite: http.SameSiteLaxMode,
+ })
+
+ // Redirect to the root path, which is the proxied application.
+ http.Redirect(w, r, "/", http.StatusFound)
+ return
+ }
+
+ w.WriteHeader(http.StatusMethodNotAllowed)
+}
+// LogoutHandler invalidates the user's token by expiring the cookie.
+func LogoutHandler(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ cookie, err := r.Cookie("auth_token")
+ if err != nil {
+ // No cookie, nothing to do.
+ http.Redirect(w, r, config.Paths.Login, http.StatusFound)
+ return
+ }
+
+ err = models.DeleteAuthToken(cookie.Value)
+ if err != nil {
+ // Log the error but still proceed with logging the user out client-side.
+ logging.AppLog.Error("Failed to delete token on logout", "error", err, "ip", ip)
+ }
+
+ // Expire the cookie by setting its max age to a negative number.
+ http.SetCookie(w, &http.Cookie{
+ Name: "auth_token",
+ Value: "",
+ Path: "/",
+ Expires: time.Unix(0, 0),
+ MaxAge: -1,
+ HttpOnly: true,
+ Secure: r.TLS != nil,
+ SameSite: http.SameSiteLaxMode,
+ })
+
+ logging.SecurityLog.Info("LOGOUT SUCCESS", "ip", ip)
+ http.Redirect(w, r, config.Paths.Login, http.StatusFound)
+}
+
+func renderRegisterWithError(w http.ResponseWriter, message string) {
+ data := map[string]interface{}{
+ "Paths": config.Paths,
+ "Error": message,
+ }
+ w.WriteHeader(http.StatusBadRequest) // Set status to indicate failure
+ templates.ExecuteTemplate(w, "register.html", data)
+}
+
+// RegisterHandler handles displaying and processing the user registration form.
+func RegisterHandler(w http.ResponseWriter, r *http.Request) {
+ ip := logging.GetClientIP(r)
+ data := map[string]interface{}{
+ "Paths": config.Paths,
+ }
+
+ if r.Method == http.MethodGet {
+ templates.ExecuteTemplate(w, "register.html", data)
+ return
+ }
+
+ if r.Method == http.MethodPost {
+ r.ParseForm()
+ username := r.FormValue("username")
+ password := r.FormValue("password")
+ confirmPassword := r.FormValue("confirm_password")
+
+ // Validate password policy
+ if ok, message := utils.ValidatePassword(password); !ok {
+ renderRegisterWithError(w, message)
+ return
+ }
+
+ if password != confirmPassword {
+ logging.AppLog.Warn("User registration failed", "reason", "passwords do not match", "ip", ip)
+ renderRegisterWithError(w, "Passwords do not match")
+ return
+ }
+
+ existingUser, err := models.GetUserByUsername(username)
+ if err != nil {
+ logging.AppLog.Error("Error checking username during registration", "error", err, "ip", ip)
+ renderRegisterWithError(w, "An internal error occurred while checking the username.")
+ return
+ }
+ if existingUser != nil {
+ logging.AppLog.Warn("User registration failed", "reason", "username is already taken", "username", username, "ip", ip)
+ renderRegisterWithError(w, "Username is already taken")
+ return
+ }
+
+ user, err := models.CreateUser(username, password, "user")
+ if err != nil {
+ logging.AppLog.Error("Failed to create user", "error", err, "username", username, "ip", ip)
+ renderRegisterWithError(w, "Failed to create user due to an internal error.")
+ return
+ }
+ logging.SecurityLog.Info("USER CREATED", "username", user.Username, "ip", ip)
+
+ // Redirect to the login page so the user can sign in.
+ http.Redirect(w, r, config.Paths.Login, http.StatusFound)
+ return
+ }
+
+ w.WriteHeader(http.StatusMethodNotAllowed)
+}
diff --git a/handlers/proxy.go b/handlers/proxy.go
new file mode 100644
index 0000000..bc31059
--- /dev/null
+++ b/handlers/proxy.go
@@ -0,0 +1,38 @@
+package handlers
+
+import (
+ "auth-proxy/logging"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+)
+
+// NewProxy creates a new reverse proxy handler using httputil.ReverseProxy.
+func NewProxy(targetURL string) http.Handler {
+ target, err := url.Parse(targetURL)
+ if err != nil {
+ panic("Invalid target URL: " + err.Error())
+ }
+
+ proxy := httputil.NewSingleHostReverseProxy(target)
+
+ // The default director is mostly fine, but we want to log and ensure the Host header is correct.
+ director := proxy.Director
+ proxy.Director = func(req *http.Request) {
+ director(req) // Run the default director
+ req.Host = target.Host // Set the Host header to the target's host
+
+ // Log the proxying action
+ ip := logging.GetClientIP(req)
+ logging.AppLog.Info("Proxying request", "url", req.URL.String(), "ip", ip)
+ }
+
+ // Custom error handler for logging
+ proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
+ ip := logging.GetClientIP(r)
+ logging.AppLog.Error("Proxy error", "error", err, "ip", ip)
+ http.Error(w, "Proxy Error", http.StatusBadGateway)
+ }
+
+ return proxy
+} \ No newline at end of file
diff --git a/handlers/user.go b/handlers/user.go
new file mode 100644
index 0000000..196405b
--- /dev/null
+++ b/handlers/user.go
@@ -0,0 +1,100 @@
+package handlers
+
+import (
+ "auth-proxy/config"
+ "auth-proxy/models"
+ "auth-proxy/types"
+ "auth-proxy/utils"
+ "encoding/json"
+ "net/http"
+)
+
+// AccountPageHandler serves the account page, passing user data to the template.
+func AccountPageHandler(w http.ResponseWriter, r *http.Request) {
+ user, ok := r.Context().Value(types.SessionDataContextKey).(*models.User)
+ if !ok || user == nil {
+ // This should ideally not happen if AuthMiddleware is working correctly.
+ http.Error(w, "User not authenticated", http.StatusUnauthorized)
+ return
+ }
+
+ data := map[string]interface{}{
+ "Paths": config.Paths,
+ "User": user,
+ }
+ templates.ExecuteTemplate(w, "account.html", data)
+}
+
+// ChangePasswordHandler handles both displaying the page (GET) and processing the update (POST).
+func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) {
+ // For GET request, just serve the page.
+ if r.Method == http.MethodGet {
+ data := map[string]interface{}{
+ "Paths": config.Paths,
+ }
+ templates.ExecuteTemplate(w, "account_password.html", data)
+ return
+ }
+
+ // POST request logic (API call)
+ if r.Method == http.MethodPost {
+ // This is a protected route, so AuthMiddleware should have put the user in the context.
+ user, ok := r.Context().Value(types.SessionDataContextKey).(*models.User)
+ if !ok || user == nil {
+ http.Error(w, "User not authenticated", http.StatusUnauthorized)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+
+ r.ParseForm()
+ currentPassword := r.FormValue("current_password")
+ newPassword := r.FormValue("new_password")
+ confirmNewPassword := r.FormValue("confirm_new_password")
+
+ if ok, message := utils.ValidatePassword(newPassword); !ok {
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"error": message})
+ return
+ }
+
+ if newPassword != confirmNewPassword {
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"error": "New passwords do not match."})
+ return
+ }
+
+ dbUser, err := models.GetUserByID(user.ID)
+ if err != nil || dbUser == nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Could not retrieve user data."})
+ return
+ }
+
+ if !models.CheckPasswordHash(currentPassword, dbUser.PasswordHash) {
+ w.WriteHeader(http.StatusUnauthorized)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Current password is incorrect."})
+ return
+ }
+
+ newPasswordHash, err := models.HashPassword(newPassword)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Failed to process new password."})
+ return
+ }
+
+ if err := models.UpdateUserPassword(dbUser.ID, newPasswordHash); err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Failed to update password."})
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{"message": "Password updated successfully."})
+ return
+ }
+
+ // Handle other methods
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+} \ No newline at end of file