diff --git a/cmd/verstak-server/server.go b/cmd/verstak-server/server.go
index 222ff44..cbe6c5d 100644
--- a/cmd/verstak-server/server.go
+++ b/cmd/verstak-server/server.go
@@ -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("
400 Bad request
Back"))
+ 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("All fields required
Back"))
+ return
+ }
+ if err := validatePassword(password); err != "" {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(400)
+ w.Write([]byte("" + err + "
Back"))
+ return
+ }
+ hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal error
Back"))
+ 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("Username or email already taken
Back"))
+ } else {
+ w.WriteHeader(500)
+ w.Write([]byte(""+err.Error()+"
Back"))
+ }
+ 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("Registration successful
Check your email to confirm (or if SMTP not configured, check server logs for the token).
Log in"))
+ 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 = `
+
+
+Verstak Sync — Регистрация
+
+
+
+`
+
const userLoginHTML = `
@@ -1435,7 +1542,7 @@ button:hover{background:#4f46e5}
-Администратор?
+Зарегистрироваться · Администратор?
`