arimelody.me/admin/accounthttp.go

373 lines
13 KiB
Go
Raw Normal View History

package admin
import (
2025-01-26 20:37:20 +00:00
"database/sql"
"fmt"
"net/http"
"net/url"
"os"
"time"
"arimelody-web/controller"
"arimelody-web/model"
"golang.org/x/crypto/bcrypt"
)
func accountHandler(app *model.AppState) http.Handler {
2025-01-21 17:13:06 +00:00
mux := http.NewServeMux()
mux.Handle("/totp-setup", totpSetupHandler(app))
mux.Handle("/totp-confirm", totpConfirmHandler(app))
mux.Handle("/totp-delete/", http.StripPrefix("/totp-delete", totpDeleteHandler(app)))
2025-01-21 17:13:06 +00:00
mux.Handle("/password", changePasswordHandler(app))
mux.Handle("/delete", deleteAccountHandler(app))
2025-01-21 17:13:06 +00:00
return mux
}
func accountIndexHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*model.Session)
dbTOTPs, err := controller.GetTOTPsForAccount(app.DB, session.Account.ID)
if err != nil {
fmt.Printf("WARN: Failed to fetch TOTPs: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
type (
TOTP struct {
model.TOTP
CreatedAtString string
}
accountResponse struct {
Session *model.Session
TOTPs []TOTP
}
)
totps := []TOTP{}
for _, totp := range dbTOTPs {
totps = append(totps, TOTP{
TOTP: totp,
CreatedAtString: totp.CreatedAt.Format("02 Jan 2006, 15:04:05"),
})
}
sessionMessage := session.Message
sessionError := session.Error
controller.SetSessionMessage(app.DB, session, "")
controller.SetSessionError(app.DB, session, "")
session.Message = sessionMessage
session.Error = sessionError
err = accountTemplate.Execute(w, accountResponse{
Session: session,
TOTPs: totps,
})
if err != nil {
fmt.Printf("WARN: Failed to render admin account page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
}
func changePasswordHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
session := r.Context().Value("session").(*model.Session)
controller.SetSessionMessage(app.DB, session, "")
controller.SetSessionError(app.DB, session, "")
r.ParseForm()
currentPassword := r.Form.Get("current-password")
if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(currentPassword)); err != nil {
controller.SetSessionError(app.DB, session, "Incorrect password.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
return
}
newPassword := r.Form.Get("new-password")
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
return
}
session.Account.Password = string(hashedPassword)
err = controller.UpdateAccount(app.DB, session.Account)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to update account password: %v\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
return
}
controller.SetSessionError(app.DB, session, "")
controller.SetSessionMessage(app.DB, session, "Password updated successfully.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
})
}
func deleteAccountHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
err := r.ParseForm()
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if !r.Form.Has("password") {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
session := r.Context().Value("session").(*model.Session)
// check password
if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil {
fmt.Printf(
"[%s] WARN: Account \"%s\" attempted account deletion with incorrect password.\n",
time.Now().Format(time.UnixDate),
session.Account.Username,
)
controller.SetSessionError(app.DB, session, "Incorrect password.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
return
}
err = controller.DeleteAccount(app.DB, session.Account.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
2025-01-21 17:13:06 +00:00
return
}
fmt.Printf(
"[%s] INFO: Account \"%s\" deleted by user request.\n",
time.Now().Format(time.UnixDate),
session.Account.Username,
2025-01-21 17:13:06 +00:00
)
controller.SetSessionAccount(app.DB, session, nil)
controller.SetSessionError(app.DB, session, "")
controller.SetSessionMessage(app.DB, session, "Account deleted successfully.")
http.Redirect(w, r, "/admin/login", http.StatusFound)
})
}
2025-01-26 20:37:20 +00:00
type totpConfirmData struct {
Session *model.Session
TOTP *model.TOTP
NameEscaped string
QRBase64Image string
}
func totpSetupHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
type totpSetupData struct {
Session *model.Session
}
session := r.Context().Value("session").(*model.Session)
err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session })
if err != nil {
fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
err := r.ParseForm()
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
name := r.FormValue("totp-name")
if len(name) == 0 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
session := r.Context().Value("session").(*model.Session)
secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
totp := model.TOTP {
AccountID: session.Account.ID,
Name: name,
Secret: string(secret),
}
err = controller.CreateTOTP(app.DB, &totp)
if err != nil {
fmt.Printf("WARN: Failed to create TOTP method: %s\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
2025-01-26 20:37:20 +00:00
err := totpSetupTemplate.Execute(w, totpConfirmData{ Session: session })
if err != nil {
fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
qrBase64Image, err := controller.GenerateQRCode(
controller.GenerateTOTPURI(session.Account.Username, totp.Secret))
if err != nil {
2025-01-26 20:37:20 +00:00
fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
}
2025-01-26 20:37:20 +00:00
err = totpConfirmTemplate.Execute(w, totpConfirmData{
Session: session,
TOTP: &totp,
NameEscaped: url.PathEscape(totp.Name),
QRBase64Image: qrBase64Image,
})
if err != nil {
fmt.Printf("WARN: Failed to render TOTP confirm page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
})
}
func totpConfirmHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
session := r.Context().Value("session").(*model.Session)
err := r.ParseForm()
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
name := r.FormValue("totp-name")
if len(name) == 0 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
code := r.FormValue("totp")
if len(code) != controller.TOTP_CODE_LENGTH {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
totp, err := controller.GetTOTP(app.DB, session.Account.ID, name)
if err != nil {
2025-01-26 20:37:20 +00:00
fmt.Printf("WARN: Failed to fetch TOTP method: %v\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
return
}
if totp == nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
2025-01-26 20:37:20 +00:00
qrBase64Image, err := controller.GenerateQRCode(
controller.GenerateTOTPURI(session.Account.Username, totp.Secret))
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
}
confirmCode := controller.GenerateTOTP(totp.Secret, 0)
if code != confirmCode {
confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
if code != confirmCodeOffset {
2025-01-26 20:37:20 +00:00
session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." }
err = totpConfirmTemplate.Execute(w, totpConfirmData{
Session: session,
TOTP: totp,
2025-01-26 20:37:20 +00:00
NameEscaped: url.PathEscape(totp.Name),
QRBase64Image: qrBase64Image,
})
2025-01-26 20:37:20 +00:00
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render TOTP setup page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
}
2025-01-26 20:37:20 +00:00
err = controller.ConfirmTOTP(app.DB, session.Account.ID, name)
if err != nil {
fmt.Printf("WARN: Failed to confirm TOTP method: %s\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
return
}
controller.SetSessionError(app.DB, session, "")
controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" created successfully.", totp.Name))
http.Redirect(w, r, "/admin/account", http.StatusFound)
})
}
func totpDeleteHandler(app *model.AppState) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if len(r.URL.Path) < 2 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
name := r.URL.Path[1:]
session := r.Context().Value("session").(*model.Session)
totp, err := controller.GetTOTP(app.DB, session.Account.ID, name)
if err != nil {
fmt.Printf("WARN: Failed to fetch TOTP method: %s\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
return
}
if totp == nil {
http.NotFound(w, r)
return
}
err = controller.DeleteTOTP(app.DB, session.Account.ID, totp.Name)
if err != nil {
fmt.Printf("WARN: Failed to delete TOTP method: %s\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
http.Redirect(w, r, "/admin/account", http.StatusFound)
return
}
controller.SetSessionError(app.DB, session, "")
controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" deleted successfully.", totp.Name))
http.Redirect(w, r, "/admin/account", http.StatusFound)
})
}