217 lines
6.4 KiB
Go
217 lines
6.4 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
jsonErr(w, 405, "POST required")
|
|
return
|
|
}
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonErr(w, 400, "invalid JSON")
|
|
return
|
|
}
|
|
if req.Username == "" || req.Email == "" || req.Password == "" {
|
|
jsonErr(w, 400, "username, email and password required")
|
|
return
|
|
}
|
|
if err := validatePassword(req.Password); err != "" {
|
|
jsonErr(w, 400, err)
|
|
return
|
|
}
|
|
if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") {
|
|
jsonErr(w, 400, "invalid email")
|
|
return
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
jsonErr(w, 500, "internal error")
|
|
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, req.Username, strings.ToLower(req.Email), string(hash), now,
|
|
)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "UNIQUE") {
|
|
jsonErr(w, 409, "username or email already taken")
|
|
return
|
|
}
|
|
jsonErr(w, 500, err.Error())
|
|
return
|
|
}
|
|
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)
|
|
log.Printf("register: confirmation token=%s for user %s", tokenStr, req.Username)
|
|
jsonOK(w, map[string]string{"status": "confirmation_sent"})
|
|
}
|
|
|
|
func (s *Server) handleConfirm(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
jsonErr(w, 405, "GET required")
|
|
return
|
|
}
|
|
tokenStr := r.URL.Query().Get("token")
|
|
if tokenStr == "" {
|
|
jsonErr(w, 400, "token required")
|
|
return
|
|
}
|
|
var userID, expiresAt string
|
|
err := s.db.QueryRow("SELECT user_id, expires_at FROM server_email_tokens WHERE token=? AND purpose='confirm'",
|
|
tokenStr).Scan(&userID, &expiresAt)
|
|
if err != nil {
|
|
jsonErr(w, 400, "invalid or expired token")
|
|
return
|
|
}
|
|
exp, err := time.Parse(time.RFC3339, expiresAt)
|
|
if err != nil || time.Now().After(exp) {
|
|
jsonErr(w, 400, "token expired")
|
|
return
|
|
}
|
|
s.db.Exec("UPDATE server_users SET confirmed=1 WHERE id=?", userID)
|
|
log.Printf("confirm: user %s confirmed email", userID)
|
|
s.db.Exec("DELETE FROM server_email_tokens WHERE token=?", tokenStr)
|
|
jsonOK(w, map[string]string{"status": "confirmed"})
|
|
}
|
|
|
|
func (s *Server) handleUserLogin(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
jsonErr(w, 405, "POST required")
|
|
return
|
|
}
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonErr(w, 400, "invalid JSON")
|
|
return
|
|
}
|
|
if req.Username == "" || req.Password == "" {
|
|
jsonErr(w, 400, "username and password required")
|
|
return
|
|
}
|
|
var userID, hash string
|
|
var confirmed, blocked int
|
|
err := s.db.QueryRow("SELECT id, password_hash, confirmed, blocked FROM server_users WHERE username=? OR email=?",
|
|
req.Username, strings.ToLower(req.Username)).Scan(&userID, &hash, &confirmed, &blocked)
|
|
if err != nil {
|
|
jsonErr(w, 401, "invalid credentials")
|
|
return
|
|
}
|
|
if blocked != 0 {
|
|
jsonErr(w, 403, "account blocked")
|
|
return
|
|
}
|
|
if confirmed == 0 {
|
|
jsonErr(w, 403, "email not confirmed")
|
|
return
|
|
}
|
|
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(req.Password)) != nil {
|
|
jsonErr(w, 401, "invalid credentials")
|
|
return
|
|
}
|
|
s.db.Exec("UPDATE server_users SET last_seen=? WHERE id=?", time.Now().UTC().Format(time.RFC3339), userID)
|
|
tok := s.userTokens.Create(userID)
|
|
jsonOK(w, map[string]string{"token": tok, "user_id": userID})
|
|
}
|
|
|
|
func (s *Server) handleForgot(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
jsonErr(w, 405, "POST required")
|
|
return
|
|
}
|
|
var req struct {
|
|
Email string `json:"email"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonErr(w, 400, "invalid JSON")
|
|
return
|
|
}
|
|
if req.Email == "" {
|
|
jsonErr(w, 400, "email required")
|
|
return
|
|
}
|
|
var userID string
|
|
err := s.db.QueryRow("SELECT id FROM server_users WHERE email=?", strings.ToLower(req.Email)).Scan(&userID)
|
|
if err != nil {
|
|
jsonOK(w, map[string]string{"status": "if email exists, reset link sent"})
|
|
return
|
|
}
|
|
tok := make([]byte, 24)
|
|
rand.Read(tok)
|
|
tokenStr := hex.EncodeToString(tok)
|
|
exp := time.Now().Add(1 * time.Hour).UTC().Format(time.RFC3339)
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
s.db.Exec("INSERT INTO server_email_tokens (token, user_id, purpose, expires_at, created_at) VALUES (?, ?, 'reset', ?, ?)",
|
|
tokenStr, userID, exp, now)
|
|
log.Printf("forgot: reset token=%s for user %s", tokenStr, userID)
|
|
jsonOK(w, map[string]string{"status": "if email exists, reset link sent"})
|
|
}
|
|
|
|
func (s *Server) handleReset(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
jsonErr(w, 405, "POST required")
|
|
return
|
|
}
|
|
var req struct {
|
|
Token string `json:"token"`
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
jsonErr(w, 400, "invalid JSON")
|
|
return
|
|
}
|
|
if req.Token == "" || req.NewPassword == "" {
|
|
jsonErr(w, 400, "token and new_password required")
|
|
return
|
|
}
|
|
if err := validatePassword(req.NewPassword); err != "" {
|
|
jsonErr(w, 400, err)
|
|
return
|
|
}
|
|
var userID, expiresAt string
|
|
err := s.db.QueryRow("SELECT user_id, expires_at FROM server_email_tokens WHERE token=? AND purpose='reset'",
|
|
req.Token).Scan(&userID, &expiresAt)
|
|
if err != nil {
|
|
jsonErr(w, 400, "invalid or expired token")
|
|
return
|
|
}
|
|
exp, err := time.Parse(time.RFC3339, expiresAt)
|
|
if err != nil || time.Now().After(exp) {
|
|
jsonErr(w, 400, "token expired")
|
|
return
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
jsonErr(w, 500, "internal error")
|
|
return
|
|
}
|
|
s.db.Exec("UPDATE server_users SET password_hash=? WHERE id=?", string(hash), userID)
|
|
s.db.Exec("DELETE FROM server_email_tokens WHERE token=?", req.Token)
|
|
jsonOK(w, map[string]string{"status": "password reset"})
|
|
}
|