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 — Регистрация + + +
+

Регистрация

+ + + + + + +
Минимум 8 символов: латинские буквы + цифры
+ +

Уже есть аккаунт? Войти

+
+` + const userLoginHTML = ` @@ -1435,7 +1542,7 @@ button:hover{background:#4f46e5} -

Администратор?

+

Зарегистрироваться · Администратор?

`