very close to rolling this out! just need to address some security concerns first
203 lines
7 KiB
Go
203 lines
7 KiB
Go
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"))
|
|
})
|
|
}
|