package admin import ( "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 { 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))) mux.Handle("/password", changePasswordHandler(app)) mux.Handle("/delete", deleteAccountHandler(app)) 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 = pages["account"].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") || !r.Form.Has("totp") { 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 } totpMethod, err := controller.CheckTOTPForAccount(app.DB, session.Account.ID, r.Form.Get("totp")) if err != nil { fmt.Fprintf(os.Stderr, "Failed to fetch account: %v\n", err) controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.") http.Redirect(w, r, "/admin/account", http.StatusFound) return } if totpMethod == nil { fmt.Printf( "[%s] WARN: Account \"%s\" attempted account deletion with incorrect TOTP.\n", time.Now().Format(time.UnixDate), session.Account.Username, ) controller.SetSessionError(app.DB, session, "Incorrect TOTP.") http.Redirect(w, r, "/admin/account", http.StatusFound) } 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) return } fmt.Printf( "[%s] INFO: Account \"%s\" deleted by user request.\n", time.Now().Format(time.UnixDate), session.Account.Username, ) 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) }) } 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 := pages["totp-setup"].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 } type totpSetupData struct { Session *model.Session TOTP *model.TOTP NameEscaped string } 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.") err := pages["totp-setup"].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 } err = pages["totp-confirm"].Execute(w, totpSetupData{ Session: session, TOTP: &totp, NameEscaped: url.PathEscape(totp.Name), }) 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 } type totpConfirmData struct { Session *model.Session TOTP *model.TOTP } 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 { 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.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } fmt.Printf( "TOTP:\n\tName: %s\n\tSecret: %s\n", totp.Name, totp.Secret, ) confirmCode := controller.GenerateTOTP(totp.Secret, 0) if code != confirmCode { confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1) if code != confirmCodeOffset { controller.SetSessionError(app.DB, session, "Incorrect TOTP code. Please try again.") err = pages["totp-confirm"].Execute(w, totpConfirmData{ Session: session, TOTP: totp, }) 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) }) }