diff --git a/backend/server/server.go b/backend/server/server.go new file mode 100644 index 0000000..d8e872d --- /dev/null +++ b/backend/server/server.go @@ -0,0 +1,677 @@ +package server + +import ( + "fmt" + "log/slog" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/yyc12345/coconut-leaf/backend/config" + "github.com/yyc12345/coconut-leaf/backend/database" + "github.com/yyc12345/coconut-leaf/backend/utils" +) + +// ResponseBody is the JSON envelope returned by every API endpoint, mirroring +// the legacy ConstructResponseBody: {success, error, data}. +type ResponseBody struct { + Success bool `json:"success"` + Error string `json:"error"` + Data any `json:"data"` +} + +// Handler holds the shared dependencies injected into every Gin handler: the +// database, the application logger and the loaded config. +type Handler struct { + DB database.Database + Logger *slog.Logger + Cfg *config.Config +} + +// region: Utilities + +// respond wraps a (data, error) pair into a ResponseBody. The HTTP status is +// always 200; failures are encoded in the body, mirroring the legacy API. +func respond(c *gin.Context, data any, err error) { + if err != nil { + c.JSON(http.StatusOK, ResponseBody{Success: false, Error: err.Error()}) + return + } + c.JSON(http.StatusOK, ResponseBody{Success: true, Data: data}) +} + +// respondInvalidParam replies with the legacy "Invalid parameter" body. +func respondInvalidParam(c *gin.Context) { + c.JSON(http.StatusOK, ResponseBody{Success: false, Error: "Invalid parameter"}) +} + +// fetchForm flattens the POST form into a map and logs it at debug level, +// mirroring the legacy SmartDbCaller "User Form" log. +func fetchForm(c *gin.Context, logger *slog.Logger) map[string]string { + _ = c.Request.ParseForm() + form := make(map[string]string, len(c.Request.PostForm)) + for k, v := range c.Request.PostForm { + if len(v) > 0 { + form[k] = v[0] + } + } + logger.Debug("User Form", "form", form) + return form +} + +// fetchClientInfo returns the client user agent and IP, mirroring the legacy +// FetchClientNetworkInfo. The real IP (X-Forwarded-For behind Nginx) comes from +// gin's c.ClientIP(). +func fetchClientInfo(c *gin.Context) (ua, ip string) { + ua = c.Request.UserAgent() + ip = c.ClientIP() + if ip == "" { + ip = "0.0.0.0" + } + return ua, ip +} + +// endregion + +// Run sets up the Gin engine with all routes bound to the handler and starts +// listening on the configured web port. +func Run(handler *Handler) error { + r := gin.Default() + registerRoutes(r, handler) + return r.Run(fmt.Sprintf(":%d", handler.Cfg.Web.Port)) +} + +func registerRoutes(r *gin.Engine, h *Handler) { + // common + r.POST("/common/salt", h.CommonSalt) + r.POST("/common/login", h.CommonLogin) + r.POST("/common/webLogin", h.CommonWebLogin) + r.POST("/common/logout", h.CommonLogout) + r.POST("/common/tokenValid", h.CommonTokenValid) + + // calendar + r.POST("/calendar/getFull", h.CalendarGetFull) + r.POST("/calendar/getList", h.CalendarGetList) + r.POST("/calendar/getDetail", h.CalendarGetDetail) + r.POST("/calendar/update", h.CalendarUpdate) + r.POST("/calendar/add", h.CalendarAdd) + r.POST("/calendar/delete", h.CalendarDelete) + + // collection + r.POST("/collection/getFullOwn", h.CollectionGetFullOwn) + r.POST("/collection/getListOwn", h.CollectionGetListOwn) + r.POST("/collection/getDetailOwn", h.CollectionGetDetailOwn) + r.POST("/collection/addOwn", h.CollectionAddOwn) + r.POST("/collection/updateOwn", h.CollectionUpdateOwn) + r.POST("/collection/deleteOwn", h.CollectionDeleteOwn) + r.POST("/collection/getSharing", h.CollectionGetSharing) + r.POST("/collection/deleteSharing", h.CollectionDeleteSharing) + r.POST("/collection/addSharing", h.CollectionAddSharing) + r.POST("/collection/getShared", h.CollectionGetShared) + + // todo + r.POST("/todo/getFull", h.TodoGetFull) + r.POST("/todo/getList", h.TodoGetList) + r.POST("/todo/getDetail", h.TodoGetDetail) + r.POST("/todo/add", h.TodoAdd) + r.POST("/todo/update", h.TodoUpdate) + r.POST("/todo/delete", h.TodoDelete) + + // admin + r.POST("/admin/get", h.AdminGet) + r.POST("/admin/add", h.AdminAdd) + r.POST("/admin/update", h.AdminUpdate) + r.POST("/admin/delete", h.AdminDelete) + + // profile + r.POST("/profile/isAdmin", h.ProfileIsAdmin) + r.POST("/profile/changePassword", h.ProfileChangePassword) + r.POST("/profile/getToken", h.ProfileGetToken) + r.POST("/profile/deleteToken", h.ProfileDeleteToken) +} + +// region: API Route + +// region: Common + +func (h *Handler) CommonSalt(c *gin.Context) { + form := fetchForm(c, h.Logger) + username, ok := form["username"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.CommonSalt(c.Request.Context(), username) + respond(c, data, err) +} + +func (h *Handler) CommonLogin(c *gin.Context) { + form := fetchForm(c, h.Logger) + username, okU := form["username"] + password, okP := form["password"] + if !okU || !okP { + respondInvalidParam(c) + return + } + clientUa, clientIp := fetchClientInfo(c) + data, err := h.DB.CommonLogin(c.Request.Context(), username, password, clientUa, clientIp) + respond(c, data, err) +} + +func (h *Handler) CommonWebLogin(c *gin.Context) { + form := fetchForm(c, h.Logger) + username, okU := form["username"] + password, okP := form["password"] + if !okU || !okP { + respondInvalidParam(c) + return + } + clientUa, clientIp := fetchClientInfo(c) + data, err := h.DB.CommonWebLogin(c.Request.Context(), username, password, clientUa, clientIp) + respond(c, data, err) +} + +func (h *Handler) CommonLogout(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.CommonLogout(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) CommonTokenValid(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.CommonTokenValid(c.Request.Context(), token) + respond(c, data, err) +} + +// endregion + +// region: Calendar + +func (h *Handler) CalendarGetFull(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + sds, okS := form["startDateTime"] + eds, okE := form["endDateTime"] + if !okT || !okS || !okE { + respondInvalidParam(c) + return + } + startDateTime, err1 := strconv.ParseInt(sds, 10, 64) + endDateTime, err2 := strconv.ParseInt(eds, 10, 64) + if err1 != nil || err2 != nil { + respondInvalidParam(c) + return + } + data, err := h.DB.CalendarGetFull(c.Request.Context(), token, startDateTime, endDateTime) + respond(c, data, err) +} + +func (h *Handler) CalendarGetList(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + sds, okS := form["startDateTime"] + eds, okE := form["endDateTime"] + if !okT || !okS || !okE { + respondInvalidParam(c) + return + } + startDateTime, err1 := strconv.ParseInt(sds, 10, 64) + endDateTime, err2 := strconv.ParseInt(eds, 10, 64) + if err1 != nil || err2 != nil { + respondInvalidParam(c) + return + } + data, err := h.DB.CalendarGetList(c.Request.Context(), token, startDateTime, endDateTime) + respond(c, data, err) +} + +func (h *Handler) CalendarGetDetail(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + if !okT || !okU { + respondInvalidParam(c) + return + } + data, err := h.DB.CalendarGetDetail(c.Request.Context(), token, uuid) + respond(c, data, err) +} + +func (h *Handler) CalendarUpdate(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + lastChange, okL := form["lastChange"] + if !okT || !okU || !okL { + respondInvalidParam(c) + return + } + + var opts database.CalendarUpdateOptions + provided := 0 + if v, ok := form["belongTo"]; ok { + opts.BelongTo = &v + provided++ + } + if v, ok := form["title"]; ok { + opts.Title = &v + provided++ + } + if v, ok := form["description"]; ok { + opts.Description = &v + provided++ + } + if v, ok := form["eventDateTimeStart"]; ok { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + respondInvalidParam(c) + return + } + opts.EventDateTimeStart = &n + provided++ + } + if v, ok := form["eventDateTimeEnd"]; ok { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + respondInvalidParam(c) + return + } + opts.EventDateTimeEnd = &n + provided++ + } + if v, ok := form["loopRules"]; ok { + opts.LoopRules = &v + provided++ + } + if v, ok := form["timezoneOffset"]; ok { + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + respondInvalidParam(c) + return + } + opts.TimezoneOffset = &n + provided++ + } + if provided == 0 { + respondInvalidParam(c) + return + } + + data, err := h.DB.CalendarUpdate(c.Request.Context(), token, uuid, lastChange, opts) + respond(c, data, err) +} + +func (h *Handler) CalendarAdd(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + belongTo, okB := form["belongTo"] + title, okTi := form["title"] + description, okD := form["description"] + loopRules, okL := form["loopRules"] + edtsStr, okES := form["eventDateTimeStart"] + edteStr, okEE := form["eventDateTimeEnd"] + tzoStr, okTZ := form["timezoneOffset"] + if !okT || !okB || !okTi || !okD || !okL || !okES || !okEE || !okTZ { + respondInvalidParam(c) + return + } + edts, err1 := strconv.ParseInt(edtsStr, 10, 64) + edte, err2 := strconv.ParseInt(edteStr, 10, 64) + tzo, err3 := strconv.ParseInt(tzoStr, 10, 64) + if err1 != nil || err2 != nil || err3 != nil { + respondInvalidParam(c) + return + } + data, err := h.DB.CalendarAdd(c.Request.Context(), token, belongTo, title, description, edts, edte, loopRules, tzo) + respond(c, data, err) +} + +func (h *Handler) CalendarDelete(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + lastChange, okL := form["lastChange"] + if !okT || !okU || !okL { + respondInvalidParam(c) + return + } + data, err := h.DB.CalendarDelete(c.Request.Context(), token, uuid, lastChange) + respond(c, data, err) +} + +// endregion + +// region: Collection + +func (h *Handler) CollectionGetFullOwn(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionGetFullOwn(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) CollectionGetListOwn(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionGetListOwn(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) CollectionGetDetailOwn(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + if !okT || !okU { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionGetDetailOwn(c.Request.Context(), token, uuid) + respond(c, data, err) +} + +func (h *Handler) CollectionAddOwn(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + name, okN := form["name"] + if !okT || !okN { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionAddOwn(c.Request.Context(), token, name) + respond(c, data, err) +} + +func (h *Handler) CollectionUpdateOwn(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + name, okN := form["name"] + lastChange, okL := form["lastChange"] + if !okT || !okU || !okN || !okL { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionUpdateOwn(c.Request.Context(), token, uuid, name, lastChange) + respond(c, data, err) +} + +func (h *Handler) CollectionDeleteOwn(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + lastChange, okL := form["lastChange"] + if !okT || !okU || !okL { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionDeleteOwn(c.Request.Context(), token, uuid, lastChange) + respond(c, data, err) +} + +func (h *Handler) CollectionGetSharing(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + if !okT || !okU { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionGetSharing(c.Request.Context(), token, uuid) + respond(c, data, err) +} + +func (h *Handler) CollectionDeleteSharing(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + target, okG := form["target"] + lastChange, okL := form["lastChange"] + if !okT || !okU || !okG || !okL { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionDeleteSharing(c.Request.Context(), token, uuid, target, lastChange) + respond(c, data, err) +} + +func (h *Handler) CollectionAddSharing(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + target, okG := form["target"] + lastChange, okL := form["lastChange"] + if !okT || !okU || !okG || !okL { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionAddSharing(c.Request.Context(), token, uuid, target, lastChange) + respond(c, data, err) +} + +func (h *Handler) CollectionGetShared(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.CollectionGetShared(c.Request.Context(), token) + respond(c, data, err) +} + +// endregion + +// region: Todo + +func (h *Handler) TodoGetFull(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.TodoGetFull(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) TodoGetList(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.TodoGetList(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) TodoGetDetail(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + if !okT || !okU { + respondInvalidParam(c) + return + } + data, err := h.DB.TodoGetDetail(c.Request.Context(), token, uuid) + respond(c, data, err) +} + +func (h *Handler) TodoAdd(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.TodoAdd(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) TodoUpdate(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + dataField, okD := form["data"] + lastChange, okL := form["lastChange"] + if !okT || !okU || !okD || !okL { + respondInvalidParam(c) + return + } + data, err := h.DB.TodoUpdate(c.Request.Context(), token, uuid, dataField, lastChange) + respond(c, data, err) +} + +func (h *Handler) TodoDelete(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + uuid, okU := form["uuid"] + lastChange, okL := form["lastChange"] + if !okT || !okU || !okL { + respondInvalidParam(c) + return + } + data, err := h.DB.TodoDelete(c.Request.Context(), token, uuid, lastChange) + respond(c, data, err) +} + +// endregion + +// region: Admin + +func (h *Handler) AdminGet(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.AdminGet(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) AdminAdd(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + username, okU := form["username"] + if !okT || !okU { + respondInvalidParam(c) + return + } + data, err := h.DB.AdminAdd(c.Request.Context(), token, username) + respond(c, data, err) +} + +func (h *Handler) AdminUpdate(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + username, okU := form["username"] + if !okT || !okU { + respondInvalidParam(c) + return + } + + var opts database.AdminUpdateOptions + provided := 0 + if v, ok := form["password"]; ok { + opts.Password = &v + provided++ + } + if v, ok := form["isAdmin"]; ok { + b := utils.Str2Bool(v) + opts.IsAdmin = &b + provided++ + } + if provided == 0 { + respondInvalidParam(c) + return + } + + data, err := h.DB.AdminUpdate(c.Request.Context(), token, username, opts) + respond(c, data, err) +} + +func (h *Handler) AdminDelete(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + username, okU := form["username"] + if !okT || !okU { + respondInvalidParam(c) + return + } + data, err := h.DB.AdminDelete(c.Request.Context(), token, username) + respond(c, data, err) +} + +// endregion + +// region: Profile + +func (h *Handler) ProfileIsAdmin(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.ProfileIsAdmin(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) ProfileChangePassword(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + password, okP := form["password"] + if !okT || !okP { + respondInvalidParam(c) + return + } + data, err := h.DB.ProfileChangePassword(c.Request.Context(), token, password) + respond(c, data, err) +} + +func (h *Handler) ProfileGetToken(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, ok := form["token"] + if !ok { + respondInvalidParam(c) + return + } + data, err := h.DB.ProfileGetToken(c.Request.Context(), token) + respond(c, data, err) +} + +func (h *Handler) ProfileDeleteToken(c *gin.Context) { + form := fetchForm(c, h.Logger) + token, okT := form["token"] + deleteToken, okD := form["deleteToken"] + if !okT || !okD { + respondInvalidParam(c) + return + } + data, err := h.DB.ProfileDeleteToken(c.Request.Context(), token, deleteToken) + respond(c, data, err) +} + +// endregion + +// endregion