269 lines
7.4 KiB
Go
269 lines
7.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
func SignupPage(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write([]byte(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Sign Up</title>
|
|
</head>
|
|
<body>
|
|
<h1>Sign Up</h1>
|
|
<form method="POST" action="/auth/signup">
|
|
<input type="email" name="email" placeholder="Email" required>
|
|
<input type="text" name="name" placeholder="Name" required>
|
|
<input type="password" name="password" placeholder="Password" required>
|
|
<button type="submit">Sign Up</button>
|
|
</form>
|
|
<p>Already have an account? <a href="/auth/login">Login</a></p>
|
|
</body>
|
|
</html>`))
|
|
}
|
|
|
|
func Signup(w http.ResponseWriter, r *http.Request) {
|
|
r.ParseForm()
|
|
email := r.FormValue("email")
|
|
name := r.FormValue("name")
|
|
password := r.FormValue("password")
|
|
|
|
if email == "" || name == "" || password == "" {
|
|
http.Error(w, "All fields required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
http.Error(w, "Failed to hash password", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
account := struct {
|
|
Email string
|
|
Name string
|
|
Password string
|
|
CreatedAt int64
|
|
}{
|
|
Email: email,
|
|
Name: name,
|
|
Password: string(hashedPassword),
|
|
CreatedAt: time.Now().Unix(),
|
|
}
|
|
|
|
_, err = DB.Exec(
|
|
"INSERT INTO accounts (email, name, password, created_at) VALUES (?, ?, ?, ?)",
|
|
account.Email, account.Name, account.Password, account.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
http.Error(w, "Email already exists", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var accountID int64
|
|
err = DB.QueryRow("SELECT account_id FROM accounts WHERE email = ?", email).Scan(&accountID)
|
|
if err != nil {
|
|
http.Error(w, "Failed to create account", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
client := struct {
|
|
AccountID int64
|
|
Name string
|
|
CreatedAt int64
|
|
}{
|
|
AccountID: accountID,
|
|
Name: name,
|
|
CreatedAt: time.Now().Unix(),
|
|
}
|
|
_, err = DB.Exec(
|
|
"INSERT INTO clients (account_id, name, created_at) VALUES (?, ?, ?)",
|
|
client.AccountID, client.Name, client.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
http.Error(w, "Failed to create client", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sessionID := generateSessionID()
|
|
expires := time.Now().Add(24 * time.Hour).Unix()
|
|
|
|
_, err = DB.Exec("INSERT INTO sessions (session_id, account_id, expires) VALUES (?, ?, ?)", sessionID, accountID, expires)
|
|
if err != nil {
|
|
http.Error(w, "Failed to create session", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{Name: "session", Value: sessionID, Path: "/"})
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
}
|
|
|
|
func LoginPage(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write([]byte(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Login</title>
|
|
</head>
|
|
<body>
|
|
<h1>Login</h1>
|
|
<form method="POST" action="/auth/login">
|
|
<input type="email" name="email" placeholder="Email" required>
|
|
<input type="password" name="password" placeholder="Password" required>
|
|
<button type="submit">Login</button>
|
|
</form>
|
|
<p>Don't have an account? <a href="/auth/signup">Sign Up</a></p>
|
|
</body>
|
|
</html>`))
|
|
}
|
|
|
|
var DB *sql.DB
|
|
|
|
func Login(w http.ResponseWriter, r *http.Request) {
|
|
r.ParseForm()
|
|
email := r.FormValue("email")
|
|
password := r.FormValue("password")
|
|
|
|
var accountID int64
|
|
var hashedPassword string
|
|
err := DB.QueryRow("SELECT account_id, password FROM accounts WHERE email = ?", email).Scan(&accountID, &hashedPassword)
|
|
if err != nil {
|
|
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)); err != nil {
|
|
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
sessionID := generateSessionID()
|
|
expires := time.Now().Add(24 * time.Hour).Unix()
|
|
|
|
_, err = DB.Exec("INSERT INTO sessions (session_id, account_id, expires) VALUES (?, ?, ?)", sessionID, accountID, expires)
|
|
if err != nil {
|
|
http.Error(w, "Failed to create session", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{Name: "session", Value: sessionID, Path: "/"})
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
}
|
|
|
|
func Logout(w http.ResponseWriter, r *http.Request) {
|
|
cookie, err := r.Cookie("session")
|
|
if err == nil {
|
|
DB.Exec("DELETE FROM sessions WHERE session_id = ?", cookie.Value)
|
|
}
|
|
|
|
http.SetCookie(w, &http.Cookie{Name: "session", Value: "", Path: "/", MaxAge: -1})
|
|
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
|
}
|
|
|
|
func generateSessionID() string {
|
|
return time.Now().Format("20060102150405") + "-" + randomString(32)
|
|
}
|
|
|
|
func randomString(n int) string {
|
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func getSession(r *http.Request) (int64, error) {
|
|
cookie, err := r.Cookie("session")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var accountID int64
|
|
err = DB.QueryRow("SELECT account_id FROM sessions WHERE session_id = ? AND expires > ?", cookie.Value, time.Now().Unix()).Scan(&accountID)
|
|
return accountID, err
|
|
}
|
|
|
|
func GetAccountID(r *http.Request) (int64, error) {
|
|
return getSession(r)
|
|
}
|
|
|
|
func requireAuth(w http.ResponseWriter, r *http.Request) (int64, bool) {
|
|
accountID, err := getSession(r)
|
|
if err != nil {
|
|
if isAPIRequest(r) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"error":"unauthorized"}`))
|
|
} else {
|
|
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
|
}
|
|
return 0, false
|
|
}
|
|
return accountID, true
|
|
}
|
|
|
|
func isAPIRequest(r *http.Request) bool {
|
|
accept := r.Header.Get("Accept")
|
|
return accept == "application/json" || r.URL.Path == "/leads/qr"
|
|
}
|
|
|
|
func SetupAuthHandlers(db *sql.DB) {
|
|
DB = db
|
|
chi.RegisterMethod("GET")
|
|
}
|
|
|
|
func AccountPage(w http.ResponseWriter, r *http.Request) {
|
|
accountID, ok := requireAuth(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var name, email string
|
|
err := DB.QueryRow("SELECT name, email FROM accounts WHERE account_id = ?", accountID).Scan(&name, &email)
|
|
if err != nil {
|
|
http.Error(w, "Account not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write([]byte(`<!DOCTYPE html><html><head><title>Account</title></head><body><h1>Account Settings</h1><form method="POST" action="/auth/account">
|
|
<p>Name: <input type="text" name="name" value="` + name + `"></p>
|
|
<p>Email: <input type="email" value="` + email + `" disabled></p>
|
|
<p>New Password: <input type="password" name="password" placeholder="Leave blank to keep current"></p>
|
|
<button type="submit">Update</button>
|
|
</form>
|
|
<a href="/">Back to Dashboard</a></body></html>`))
|
|
}
|
|
|
|
func UpdateAccount(w http.ResponseWriter, r *http.Request) {
|
|
accountID, ok := requireAuth(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
r.ParseForm()
|
|
name := r.FormValue("name")
|
|
password := r.FormValue("password")
|
|
|
|
if name != "" {
|
|
DB.Exec("UPDATE accounts SET name = ? WHERE account_id = ?", name, accountID)
|
|
}
|
|
|
|
if password != "" {
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err == nil {
|
|
DB.Exec("UPDATE accounts SET password = ? WHERE account_id = ?", string(hashedPassword), accountID)
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, "/auth/account", http.StatusFound)
|
|
} |