feat: add user registration web form at /register

This commit is contained in:
mirivlad 2026-06-01 23:46:25 +08:00
parent 0ef54c31f8
commit 99e47fcb17
1 changed files with 108 additions and 1 deletions

View File

@ -324,6 +324,7 @@ func (s *Server) routes() *http.ServeMux {
mux.HandleFunc("/api/v1/auth/forgot", s.handleForgot)
mux.HandleFunc("/api/v1/auth/reset", s.handleReset)
mux.HandleFunc("/api/v1/user/devices", s.handleUserDevices)
mux.HandleFunc("/register", s.handleUserWebRegister)
mux.HandleFunc("/login", s.handleUserWebLogin)
mux.HandleFunc("/dashboard", s.handleUserDashboard)
mux.HandleFunc("/logout", s.handleUserWebLogout)
@ -1041,6 +1042,82 @@ func (s *Server) requireUserWeb(w http.ResponseWriter, r *http.Request) (string,
return userID, true
}
func (s *Server) handleUserWebRegister(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(userRegisterHTML))
case "POST":
if err := r.ParseForm(); err != nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(400)
w.Write([]byte("<html><body><h1>400 Bad request</h1><a href='/register'>Back</a></body></html>"))
return
}
username := r.FormValue("username")
email := r.FormValue("email")
password := r.FormValue("password")
if username == "" || email == "" || password == "" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(400)
w.Write([]byte("<html><body><h1>All fields required</h1><a href='/register'>Back</a></body></html>"))
return
}
if err := validatePassword(password); err != "" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(400)
w.Write([]byte("<html><body><h1>" + err + "</h1><a href='/register'>Back</a></body></html>"))
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("<html><body><h1>Internal error</h1><a href='/register'>Back</a></body></html>"))
return
}
now := time.Now().UTC().Format(time.RFC3339)
id := make([]byte, 12)
rand.Read(id)
userID := hex.EncodeToString(id)
_, err = s.db.Exec(
"INSERT INTO server_users (id, username, email, password_hash, confirmed, created_at) VALUES (?, ?, ?, ?, 0, ?)",
userID, username, strings.ToLower(email), string(hash), now,
)
if err != nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if strings.Contains(err.Error(), "UNIQUE") {
w.WriteHeader(409)
w.Write([]byte("<html><body><h1>Username or email already taken</h1><a href='/register'>Back</a></body></html>"))
} else {
w.WriteHeader(500)
w.Write([]byte("<html><body><h1>"+err.Error()+"</h1><a href='/register'>Back</a></body></html>"))
}
return
}
// Confirmation token.
tok := make([]byte, 24)
rand.Read(tok)
tokenStr := hex.EncodeToString(tok)
exp := time.Now().Add(48 * time.Hour).UTC().Format(time.RFC3339)
s.db.Exec("INSERT INTO server_email_tokens (token, user_id, purpose, expires_at, created_at) VALUES (?, ?, 'confirm', ?, ?)",
tokenStr, userID, exp, now)
// Try to send email.
host := s.smtpGet("smtp_host")
if host != "" {
confirmURL := fmt.Sprintf("%s/api/v1/auth/confirm?token=%s", s.smtpGet("server_url"), tokenStr)
if confirmURL == fmt.Sprintf("/api/v1/auth/confirm?token=%s", tokenStr) {
confirmURL = fmt.Sprintf("http://%s/api/v1/auth/confirm?token=%s", r.Host, tokenStr)
}
body := fmt.Sprintf("Welcome to Verstak Sync!\n\nPlease confirm your email by clicking:\n%s\n\nIf you did not register, ignore this message.", confirmURL)
s.smtpSend(email, "Confirm your Verstak Sync account", body)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte("<html><body><h1>Registration successful</h1><p>Check your email to confirm (or if SMTP not configured, check server logs for the token).</p><a href='/login'>Log in</a></body></html>"))
default:
jsonErr(w, 405, "method not allowed")
}
}
func (s *Server) handleUserWebLogin(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
@ -1414,6 +1491,36 @@ func (s *Server) handleAdminAPI(w http.ResponseWriter, r *http.Request) {
// Embedded admin login HTML
// ============================================================
const userRegisterHTML = `<!DOCTYPE html>
<html lang="ru">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Verstak Sync Регистрация</title>
<style>body{font-family:sans-serif;background:#13131f;color:#e4e4ef;display:flex;justify-content:center;align-items:center;height:100vh;margin:0}
form{background:#1a1a28;border:1px solid #2a2a3c;border-radius:12px;padding:32px;width:320px}
h1{font-size:20px;margin:0 0 20px;text-align:center}
p{text-align:center;font-size:12px;color:#666;margin-top:16px}
a{color:#6366f1}
label{display:block;font-size:12px;color:#888;margin-bottom:4px}
input{width:100%;padding:8px 12px;border:1px solid #2a2a3c;background:#13131f;color:#e4e4ef;border-radius:6px;font-size:14px;margin-bottom:16px;box-sizing:border-box}
button{width:100%;padding:10px;background:#6366f1;color:#fff;border:none;border-radius:6px;font-size:14px;cursor:pointer}
button:hover{background:#4f46e5}
.hint{font-size:11px;color:#666;margin-top:-12px;margin-bottom:16px;text-align:center}
</style>
</head><body>
<form method="POST">
<h1>Регистрация</h1>
<label>Логин</label>
<input type="text" name="username" autofocus required>
<label>Email</label>
<input type="email" name="email" required>
<label>Пароль</label>
<input type="password" name="password" required minlength="8">
<div class="hint">Минимум 8 символов: латинские буквы + цифры</div>
<button>Зарегистрироваться</button>
<p>Уже есть аккаунт? <a href="/login">Войти</a></p>
</form>
</body></html>`
const userLoginHTML = `<!DOCTYPE html>
<html lang="ru">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@ -1435,7 +1542,7 @@ button:hover{background:#4f46e5}</style>
<label>Пароль</label>
<input type="password" name="password" required>
<button>Войти</button>
<p><a href="/admin/login">Администратор?</a></p>
<p><a href="/register">Зарегистрироваться</a> · <a href="/admin/login">Администратор?</a></p>
</form>
</body></html>`