feat: add user registration web form at /register
This commit is contained in:
parent
0ef54c31f8
commit
99e47fcb17
|
|
@ -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>`
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue