package api import ( "arimelody-web/controller" "arimelody-web/model" "arimelody-web/global" "encoding/json" "fmt" "net/http" "os" "strings" "time" "golang.org/x/crypto/bcrypt" ) func handleLogin() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) return } type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` } credentials := LoginRequest{} err := json.NewDecoder(r.Body).Decode(&credentials) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } account, err := controller.GetAccount(global.DB, credentials.Username) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if account == nil { http.Error(w, "Invalid username or password", http.StatusBadRequest) return } err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) if err != nil { http.Error(w, "Invalid username or password", http.StatusBadRequest) return } token, err := controller.CreateToken(global.DB, account.ID, r.UserAgent()) type LoginResponse struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` } err = json.NewEncoder(w).Encode(LoginResponse{ Token: token.Token, ExpiresAt: token.ExpiresAt, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to return session token: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func handleAccountRegistration() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) return } type RegisterRequest struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` Invite string `json:"invite"` } credentials := RegisterRequest{} err := json.NewDecoder(r.Body).Decode(&credentials) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } // make sure code exists in DB invite, err := controller.GetInvite(global.DB, credentials.Invite) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if invite == nil { http.Error(w, "Invalid invite code", http.StatusBadRequest) return } if time.Now().After(invite.ExpiresAt) { err := controller.DeleteInvite(global.DB, invite.Code) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } http.Error(w, "Invalid invite code", http.StatusBadRequest) return } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } account := model.Account{ Username: credentials.Username, Password: string(hashedPassword), Email: credentials.Email, AvatarURL: "/img/default-avatar.png", } err = controller.CreateAccount(global.DB, &account) if err != nil { if strings.HasPrefix(err.Error(), "pq: duplicate key") { http.Error(w, "An account with that username already exists", http.StatusBadRequest) return } fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = controller.DeleteInvite(global.DB, invite.Code) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } token, err := controller.CreateToken(global.DB, account.ID, r.UserAgent()) type LoginResponse struct { Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` } err = json.NewEncoder(w).Encode(LoginResponse{ Token: token.Token, ExpiresAt: token.ExpiresAt, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to return session token: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }) } func handleDeleteAccount() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) return } type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` } credentials := LoginRequest{} err := json.NewDecoder(r.Body).Decode(&credentials) if err != nil { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } account, err := controller.GetAccount(global.DB, credentials.Username) if err != nil { if strings.Contains(err.Error(), "no rows") { http.Error(w, "Invalid username or password", http.StatusBadRequest) return } fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) if err != nil { http.Error(w, "Invalid password", http.StatusBadRequest) return } // TODO: check TOTP err = controller.DeleteAccount(global.DB, account.Username) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete account: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write([]byte("Account deleted successfully\n")) }) }