package admin import ( "database/sql" "fmt" "net/http" "net/url" "os" "arimelody-web/controller" "arimelody-web/log" "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 = 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 } app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r)) 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 { app.Log.Warn(log.TYPE_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r)) 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) return } app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r)) 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) }) } 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.") 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 { fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err) } 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 { 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 } 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 { session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." } err = totpConfirmTemplate.Execute(w, totpConfirmData{ Session: session, TOTP: totp, NameEscaped: url.PathEscape(totp.Name), QRBase64Image: qrBase64Image, }) 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 } } 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 } app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" created TOTP method \"%s\".", session.Account.Username, totp.Name) 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 } app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" deleted TOTP method \"%s\".", session.Account.Username, totp.Name) 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) }) }