feat(admin): add draft task creation
This commit is contained in:
+141
-12
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"cmbuyer/admin/internal/auth"
|
||||
"cmbuyer/admin/internal/tasks"
|
||||
"cmbuyer/admin/internal/transport/webui"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -22,11 +23,12 @@ type Options struct {
|
||||
AdminUsername string
|
||||
AdminPasswordBcrypt string
|
||||
Sessions *auth.Manager
|
||||
Tasks tasks.Store
|
||||
}
|
||||
|
||||
// NewRouter 返回当前服务范围内的完整 HTTP 路由。
|
||||
func NewRouter(options Options) (*gin.Engine, error) {
|
||||
if options.AdminUsername == "" || options.AdminPasswordBcrypt == "" || options.Sessions == nil {
|
||||
if options.AdminUsername == "" || options.AdminPasswordBcrypt == "" || options.Sessions == nil || options.Tasks == nil {
|
||||
return nil, errors.New("server authentication options are incomplete")
|
||||
}
|
||||
|
||||
@@ -38,6 +40,8 @@ func NewRouter(options Options) (*gin.Engine, error) {
|
||||
router.POST("/login", login(options))
|
||||
router.POST("/logout", logout(options))
|
||||
router.GET("/tasks", tasksPage(options))
|
||||
router.GET("/tasks/new", newTaskPage(options))
|
||||
router.POST("/tasks", createTask(options))
|
||||
|
||||
return router, nil
|
||||
}
|
||||
@@ -70,11 +74,14 @@ func loginPage(options Options) gin.HandlerFunc {
|
||||
|
||||
func login(options Options) gin.HandlerFunc {
|
||||
return func(context *gin.Context) {
|
||||
limitFormBody(context)
|
||||
csrfToken := context.PostForm("csrf_token")
|
||||
returnPath := returnTo(context.PostForm("return_to"))
|
||||
username := context.PostForm("username")
|
||||
password := context.PostForm("password")
|
||||
if !parseForm(context) {
|
||||
return
|
||||
}
|
||||
form := context.Request.PostForm
|
||||
csrfToken := form.Get("csrf_token")
|
||||
returnPath := returnTo(form.Get("return_to"))
|
||||
username := form.Get("username")
|
||||
password := form.Get("password")
|
||||
|
||||
if _, ok := options.Sessions.VerifyCSRF(context.Request, csrfToken); !ok {
|
||||
newCSRF, _ := options.Sessions.Ensure(context.Writer, context.Request)
|
||||
@@ -97,8 +104,10 @@ func login(options Options) gin.HandlerFunc {
|
||||
|
||||
func logout(options Options) gin.HandlerFunc {
|
||||
return func(context *gin.Context) {
|
||||
limitFormBody(context)
|
||||
authenticated, ok := options.Sessions.VerifyCSRF(context.Request, context.PostForm("csrf_token"))
|
||||
if !parseForm(context) {
|
||||
return
|
||||
}
|
||||
authenticated, ok := options.Sessions.VerifyCSRF(context.Request, context.Request.PostForm.Get("csrf_token"))
|
||||
if !ok || !authenticated {
|
||||
context.Status(http.StatusForbidden)
|
||||
return
|
||||
@@ -117,10 +126,111 @@ func tasksPage(options Options) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
context.Header("Content-Type", "text/html; charset=utf-8")
|
||||
if err := webui.RenderTasks(context.Writer, webui.TasksData{CSRFToken: csrfToken}); err != nil {
|
||||
_ = context.Error(err)
|
||||
drafts, err := options.Tasks.ListDrafts(context.Request.Context())
|
||||
if err != nil {
|
||||
context.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data := webui.TasksData{CSRFToken: csrfToken, Drafts: drafts}
|
||||
for _, draft := range drafts {
|
||||
if draft.ID == context.Query("created") {
|
||||
data.Success = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if context.Query("create") == "1" {
|
||||
form, err := newTaskForm()
|
||||
if err != nil {
|
||||
context.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data.OpenForm = true
|
||||
data.Form = form
|
||||
data.FocusField = "title"
|
||||
}
|
||||
renderTasks(context, http.StatusOK, data)
|
||||
}
|
||||
}
|
||||
|
||||
func newTaskPage(options Options) gin.HandlerFunc {
|
||||
return func(context *gin.Context) {
|
||||
csrf, authenticated := options.Sessions.Ensure(context.Writer, context.Request)
|
||||
if !authenticated {
|
||||
context.Redirect(http.StatusSeeOther, "/login?return_to=%2Ftasks%2Fnew")
|
||||
return
|
||||
}
|
||||
form, err := newTaskForm()
|
||||
if err != nil {
|
||||
context.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
renderTasks(context, http.StatusOK, webui.TasksData{CSRFToken: csrf, Form: form, FullPage: true, FocusField: "title"})
|
||||
}
|
||||
}
|
||||
func createTask(options Options) gin.HandlerFunc {
|
||||
return func(context *gin.Context) {
|
||||
if !parseForm(context) {
|
||||
return
|
||||
}
|
||||
requestForm := context.Request.PostForm
|
||||
authenticated, csrfOK := options.Sessions.VerifyCSRF(context.Request, requestForm.Get("csrf_token"))
|
||||
if !csrfOK || !authenticated {
|
||||
context.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
form := taskForm(requestForm)
|
||||
draft, validation := tasks.Validate(form)
|
||||
if draft.GoodsID != "" {
|
||||
form.ProductURL = tasks.CanonicalURL(draft.GoodsID)
|
||||
}
|
||||
fullPage := requestForm.Get("form_mode") == "full"
|
||||
if !validation.Valid() {
|
||||
drafts, err := options.Tasks.ListDrafts(context.Request.Context())
|
||||
if err != nil {
|
||||
context.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
renderTasks(context, http.StatusBadRequest, webui.TasksData{CSRFToken: csrfFor(context, options), Drafts: drafts, Form: form, Errors: validation, OpenForm: !fullPage, FullPage: fullPage, FocusField: firstError(validation)})
|
||||
return
|
||||
}
|
||||
created, err := options.Tasks.CreateDraft(context.Request.Context(), draft)
|
||||
if err != nil {
|
||||
if errors.Is(err, tasks.ErrCreateKeyConflict) {
|
||||
validation["create_key"] = "该创建请求已用于另一条任务,请重新打开表单。"
|
||||
drafts, listErr := options.Tasks.ListDrafts(context.Request.Context())
|
||||
if listErr != nil {
|
||||
context.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
renderTasks(context, http.StatusConflict, webui.TasksData{CSRFToken: csrfFor(context, options), Drafts: drafts, Form: form, Errors: validation, OpenForm: !fullPage, FullPage: fullPage, FocusField: firstError(validation)})
|
||||
return
|
||||
}
|
||||
context.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
context.Redirect(http.StatusSeeOther, "/tasks?created="+url.QueryEscape(created.ID))
|
||||
}
|
||||
}
|
||||
|
||||
func newTaskForm() (tasks.Form, error) {
|
||||
key, err := tasks.NewCreateKey()
|
||||
if err != nil {
|
||||
return tasks.Form{}, err
|
||||
}
|
||||
return tasks.Form{CreateKey: key}, nil
|
||||
}
|
||||
func taskForm(form url.Values) tasks.Form {
|
||||
return tasks.Form{CreateKey: form.Get("create_key"), Title: form.Get("title"), ProductURL: form.Get("product_url"), SKUColor: form.Get("sku_color"), SKUSize: form.Get("sku_size"), Quantity: form.Get("quantity"), MaxTotalPrice: form.Get("max_total_price")}
|
||||
}
|
||||
func csrfFor(context *gin.Context, options Options) string {
|
||||
csrf, _ := options.Sessions.Ensure(context.Writer, context.Request)
|
||||
return csrf
|
||||
}
|
||||
func renderTasks(context *gin.Context, status int, data webui.TasksData) {
|
||||
context.Header("Content-Type", "text/html; charset=utf-8")
|
||||
context.Status(status)
|
||||
if err := webui.RenderTasks(context.Writer, data); err != nil {
|
||||
_ = context.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,8 +247,27 @@ func renderLogin(context *gin.Context, status int, csrfToken, returnPath, userna
|
||||
}
|
||||
}
|
||||
|
||||
func limitFormBody(context *gin.Context) {
|
||||
func parseForm(context *gin.Context) bool {
|
||||
context.Request.Body = http.MaxBytesReader(context.Writer, context.Request.Body, maxFormBytes)
|
||||
if err := context.Request.ParseForm(); err != nil {
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
context.Status(http.StatusRequestEntityTooLarge)
|
||||
} else {
|
||||
context.Status(http.StatusBadRequest)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func firstError(validation tasks.Errors) string {
|
||||
for _, field := range []string{"title", "product_url", "sku_color", "sku_size", "quantity", "max_total_price"} {
|
||||
if _, ok := validation[field]; ok {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return "title"
|
||||
}
|
||||
|
||||
func returnTo(value string) string {
|
||||
|
||||
Reference in New Issue
Block a user