Unverified Commit 4491c751 authored by Lincoln Nogueira's avatar Lincoln Nogueira Committed by GitHub

feat: add SwaggerUI and v1 API docs (#2115)

* - Refactor several API routes from anonymous functions to regular definitions. Required to add parseable documentation comments.

- Add API documentation comments using Swag Declarative Comments Format

- Add echo-swagger to serve Swagger-UI at /api/index.html

- Fix error response from extraneous parameter resourceId to relatedMemoId in DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType")

- Add an auto-generated ./docs/api/v1.md for quick reference on repo (generated by swagger-markdown)

- Add auxiliary scripts to generate docs.go and swagger.yaml

* fix: golangci-lint errors

* fix: go fmt flag in swag scripts
parent 513002ff
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
...@@ -32,8 +32,26 @@ type SignUp struct { ...@@ -32,8 +32,26 @@ type SignUp struct {
} }
func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
// POST /auth/signin - Sign in. g.POST("/auth/signin", s.signIn)
g.POST("/auth/signin", func(c echo.Context) error { g.POST("/auth/signin/sso", s.signInSSO)
g.POST("/auth/signout", s.signOut)
g.POST("/auth/signup", s.signUp)
}
// signIn godoc
//
// @Summary Sign-in to memos.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SignIn true "Sign-in object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signin request"
// @Failure 401 {object} nil "Password login is deactivated | Incorrect login credentials, please try again"
// @Failure 403 {object} nil "User has been archived with username %s"
// @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting | Incorrect login credentials, please try again | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signin [POST]
func (s *APIV1Service) signIn(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
signin := &SignIn{} signin := &SignIn{}
...@@ -84,10 +102,23 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { ...@@ -84,10 +102,23 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
} }
userMessage := convertUserFromStore(user) userMessage := convertUserFromStore(user)
return c.JSON(http.StatusOK, userMessage) return c.JSON(http.StatusOK, userMessage)
}) }
// POST /auth/signin/sso - Sign in with SSO // signInSSO godoc
g.POST("/auth/signin/sso", func(c echo.Context) error { //
// @Summary Sign-in to memos using SSO.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SSOSignIn true "SSO sign-in object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signin request"
// @Failure 401 {object} nil "Access denied, identifier does not match the filter."
// @Failure 403 {object} nil "User has been archived with username {username}"
// @Failure 404 {object} nil "Identity provider not found"
// @Failure 500 {object} nil "Failed to find identity provider | Failed to create identity provider instance | Failed to exchange token | Failed to get user info | Failed to compile identifier filter | Incorrect login credentials, please try again | Failed to generate random password | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signin/sso [POST]
func (s *APIV1Service) signInSSO(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
signin := &SSOSignIn{} signin := &SSOSignIn{}
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil { if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
...@@ -172,10 +203,35 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { ...@@ -172,10 +203,35 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
} }
userMessage := convertUserFromStore(user) userMessage := convertUserFromStore(user)
return c.JSON(http.StatusOK, userMessage) return c.JSON(http.StatusOK, userMessage)
}) }
// POST /auth/signup - Sign up a new user. // signOut godoc
g.POST("/auth/signup", func(c echo.Context) error { //
// @Summary Sign-out from memos.
// @Tags auth
// @Produce json
// @Success 200 {boolean} true "Sign-out success"
// @Router /api/v1/auth/signout [POST]
func (*APIV1Service) signOut(c echo.Context) error {
RemoveTokensAndCookies(c)
return c.JSON(http.StatusOK, true)
}
// signUp godoc
//
// @Summary Sign-up to memos.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body SignUp true "Sign-up object"
// @Success 200 {object} store.User "User information"
// @Failure 400 {object} nil "Malformatted signup request | Failed to find users"
// @Failure 401 {object} nil "signup is disabled"
// @Failure 403 {object} nil "Forbidden"
// @Failure 404 {object} nil "Not found"
// @Failure 500 {object} nil "Failed to find system setting | Failed to unmarshal system setting allow signup | Failed to generate password hash | Failed to create user | Failed to generate tokens | Failed to create activity"
// @Router /api/v1/auth/signup [POST]
func (s *APIV1Service) signUp(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
signup := &SignUp{} signup := &SignUp{}
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil { if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
...@@ -239,13 +295,6 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { ...@@ -239,13 +295,6 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) {
userMessage := convertUserFromStore(user) userMessage := convertUserFromStore(user)
return c.JSON(http.StatusOK, userMessage) return c.JSON(http.StatusOK, userMessage)
})
// POST /auth/signout - Sign out.
g.POST("/auth/signout", func(c echo.Context) error {
RemoveTokensAndCookies(c)
return c.JSON(http.StatusOK, true)
})
} }
func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *store.User) error { func (s *APIV1Service) createAuthSignInActivity(c echo.Context, user *store.User) error {
......
...@@ -11,7 +11,23 @@ import ( ...@@ -11,7 +11,23 @@ import (
func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) { func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) {
// GET /get/httpmeta?url={url} - Get website meta. // GET /get/httpmeta?url={url} - Get website meta.
g.GET("/get/httpmeta", func(c echo.Context) error { g.GET("/get/httpmeta", httpmeta)
// GET /get/image?url={url} - Get image.
g.GET("/get/image", image)
}
// httpmeta godoc
//
// @Summary Get website metadata
// @Tags get
// @Produce json
// @Param url query string true "Website URL"
// @Success 200 {object} getter.HTMLMeta "Extracted metadata"
// @Failure 400 {object} nil "Missing website url | Wrong url"
// @Failure 406 {object} nil "Failed to get website meta with url: %s"
// @Router /o/get/httpmeta [GET]
func httpmeta(c echo.Context) error {
urlStr := c.QueryParam("url") urlStr := c.QueryParam("url")
if urlStr == "" { if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing website url") return echo.NewHTTPError(http.StatusBadRequest, "Missing website url")
...@@ -25,10 +41,19 @@ func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) { ...@@ -25,10 +41,19 @@ func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusNotAcceptable, fmt.Sprintf("Failed to get website meta with url: %s", urlStr)).SetInternal(err) return echo.NewHTTPError(http.StatusNotAcceptable, fmt.Sprintf("Failed to get website meta with url: %s", urlStr)).SetInternal(err)
} }
return c.JSON(http.StatusOK, htmlMeta) return c.JSON(http.StatusOK, htmlMeta)
}) }
// GET /get/image?url={url} - Get image. // image godoc
g.GET("/get/image", func(c echo.Context) error { //
// @Summary Get image from URL
// @Tags get
// @Produce image/*
// @Param url query string true "Image url"
// @Success 200 {object} nil "Image"
// @Failure 400 {object} nil "Missing image url | Wrong url | Failed to get image url: %s"
// @Failure 500 {object} nil "Failed to write image blob"
// @Router /o/get/image [GET]
func image(c echo.Context) error {
urlStr := c.QueryParam("url") urlStr := c.QueryParam("url")
if urlStr == "" { if urlStr == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Missing image url") return echo.NewHTTPError(http.StatusBadRequest, "Missing image url")
...@@ -49,5 +74,4 @@ func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) { ...@@ -49,5 +74,4 @@ func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to write image blob").SetInternal(err)
} }
return nil return nil
})
} }
This diff is collapsed.
This diff is collapsed.
...@@ -22,7 +22,25 @@ type UpsertMemoOrganizerRequest struct { ...@@ -22,7 +22,25 @@ type UpsertMemoOrganizerRequest struct {
} }
func (s *APIV1Service) registerMemoOrganizerRoutes(g *echo.Group) { func (s *APIV1Service) registerMemoOrganizerRoutes(g *echo.Group) {
g.POST("/memo/:memoId/organizer", func(c echo.Context) error { g.POST("/memo/:memoId/organizer", s.organizeMemo)
}
// organizeMemo godoc
//
// @Summary Organize memo (pin/unpin)
// @Tags memo-organizer
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to organize"
// @Param body body UpsertMemoOrganizerRequest true "Memo organizer object"
// @Success 200 {object} store.Memo "Memo information"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted post memo organizer request"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 404 {object} nil "Memo not found: %v"
// @Failure 500 {object} nil "Failed to find memo | Failed to upsert memo organizer | Failed to find memo by ID: %v | Failed to compose memo response"
// @Security ApiKeyAuth
// @Router /api/v1/memo/{memoId}/organizer [POST]
func (s *APIV1Service) organizeMemo(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId")) memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil { if err != nil {
...@@ -77,5 +95,4 @@ func (s *APIV1Service) registerMemoOrganizerRoutes(g *echo.Group) { ...@@ -77,5 +95,4 @@ func (s *APIV1Service) registerMemoOrganizerRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to compose memo response").SetInternal(err)
} }
return c.JSON(http.StatusOK, memoResponse) return c.JSON(http.StatusOK, memoResponse)
})
} }
...@@ -29,7 +29,57 @@ type UpsertMemoRelationRequest struct { ...@@ -29,7 +29,57 @@ type UpsertMemoRelationRequest struct {
} }
func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) { func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) {
g.POST("/memo/:memoId/relation", func(c echo.Context) error { g.GET("/memo/:memoId/relation", s.getMemoRelationList)
g.POST("/memo/:memoId/relation", s.createMemoRelation)
g.DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType", s.deleteMemoRelation)
}
// getMemoRelationList godoc
//
// @Summary Get a list of Memo Relations
// @Tags memo-relation
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to find relations"
// @Success 200 {object} []store.MemoRelation "Memo relation information list"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 500 {object} nil "Failed to list memo relations"
// @Router /api/v1/memo/{memoId}/relation [GET]
func (s *APIV1Service) getMemoRelationList(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoRelationList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list memo relations").SetInternal(err)
}
return c.JSON(http.StatusOK, memoRelationList)
}
// createMemoRelation godoc
//
// @Summary Create Memo Relation
// @Description Create a relation between two memos
// @Tags memo-relation
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to relate"
// @Param body body UpsertMemoRelationRequest true "Memo relation object"
// @Success 200 {object} store.MemoRelation "Memo relation information"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted post memo relation request"
// @Failure 500 {object} nil "Failed to upsert memo relation"
// @Router /api/v1/memo/{memoId}/relation [POST]
//
// NOTES:
// - Currently not secured
// - It's possible to create relations to memos that doesn't exist, which will trigger 404 errors when the frontend tries to load them.
// - It's possible to create multiple relations, though the interface only shows first.
func (s *APIV1Service) createMemoRelation(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId")) memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil { if err != nil {
...@@ -50,25 +100,27 @@ func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) { ...@@ -50,25 +100,27 @@ func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo relation").SetInternal(err)
} }
return c.JSON(http.StatusOK, memoRelation) return c.JSON(http.StatusOK, memoRelation)
}) }
g.GET("/memo/:memoId/relation", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
memoRelationList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{
MemoID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list memo relations").SetInternal(err)
}
return c.JSON(http.StatusOK, memoRelationList)
})
g.DELETE("/memo/:memoId/relation/:relatedMemoId/type/:relationType", func(c echo.Context) error { // deleteMemoRelation godoc
//
// @Summary Delete a Memo Relation
// @Description Removes a relation between two memos
// @Tags memo-relation
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to find relations"
// @Param relatedMemoId path int true "ID of memo to remove relation to"
// @Param relationType path MemoRelationType true "Type of relation to remove"
// @Success 200 {boolean} true "Memo relation deleted"
// @Failure 400 {object} nil "Memo ID is not a number: %s | Related memo ID is not a number: %s"
// @Failure 500 {object} nil "Failed to delete memo relation"
// @Router /api/v1/memo/{memoId}/relation/{relatedMemoId}/type/{relationType} [DELETE]
//
// NOTES:
// - Currently not secured.
// - Will always return true, even if the relation doesn't exist.
func (s *APIV1Service) deleteMemoRelation(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId")) memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil { if err != nil {
...@@ -76,7 +128,7 @@ func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) { ...@@ -76,7 +128,7 @@ func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) {
} }
relatedMemoID, err := util.ConvertStringToInt32(c.Param("relatedMemoId")) relatedMemoID, err := util.ConvertStringToInt32(c.Param("relatedMemoId"))
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Related memo ID is not a number: %s", c.Param("resourceId"))).SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Related memo ID is not a number: %s", c.Param("relatedMemoId"))).SetInternal(err)
} }
relationType := store.MemoRelationType(c.Param("relationType")) relationType := store.MemoRelationType(c.Param("relationType"))
...@@ -88,7 +140,6 @@ func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) { ...@@ -88,7 +140,6 @@ func (s *APIV1Service) registerMemoRelationRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo relation").SetInternal(err)
} }
return c.JSON(http.StatusOK, true) return c.JSON(http.StatusOK, true)
})
} }
func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *MemoRelation { func convertMemoRelationFromStore(memoRelation *store.MemoRelation) *MemoRelation {
......
...@@ -35,7 +35,60 @@ type MemoResourceDelete struct { ...@@ -35,7 +35,60 @@ type MemoResourceDelete struct {
} }
func (s *APIV1Service) registerMemoResourceRoutes(g *echo.Group) { func (s *APIV1Service) registerMemoResourceRoutes(g *echo.Group) {
g.POST("/memo/:memoId/resource", func(c echo.Context) error { g.GET("/memo/:memoId/resource", s.getMemoResourceList)
g.POST("/memo/:memoId/resource", s.bindMemoResource)
g.DELETE("/memo/:memoId/resource/:resourceId", s.unbindMemoResource)
}
// getMemoResourceList godoc
//
// @Summary Get resource list of a memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to fetch resource list from"
// @Success 200 {object} []Resource "Memo resource list"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 500 {object} nil "Failed to fetch resource list"
// @Router /api/v1/memo/{memoId}/resource [GET]
func (s *APIV1Service) getMemoResourceList(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
list, err := s.Store.ListResources(ctx, &store.FindResource{
MemoID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
resourceList := []*Resource{}
for _, resource := range list {
resourceList = append(resourceList, convertResourceFromStore(resource))
}
return c.JSON(http.StatusOK, resourceList)
}
// bindMemoResource godoc
//
// @Summary Bind resource to memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to bind resource to"
// @Param body body UpsertMemoResourceRequest true "Memo resource request object"
// @Success 200 {boolean} true "Memo resource binded"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted post memo resource request | Resource not found"
// @Failure 401 {object} nil "Missing user in session | Unauthorized to bind this resource"
// @Failure 500 {object} nil "Failed to fetch resource | Failed to upsert memo resource"
// @Security ApiKeyAuth
// @Router /api/v1/memo/{memoId}/resource [POST]
//
// NOTES:
// - Passing 0 to updatedTs will set it to 0 in the database, which is probably unwanted.
func (s *APIV1Service) bindMemoResource(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId")) memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil { if err != nil {
...@@ -74,29 +127,23 @@ func (s *APIV1Service) registerMemoResourceRoutes(g *echo.Group) { ...@@ -74,29 +127,23 @@ func (s *APIV1Service) registerMemoResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
} }
return c.JSON(http.StatusOK, true) return c.JSON(http.StatusOK, true)
}) }
g.GET("/memo/:memoId/resource", func(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
list, err := s.Store.ListResources(ctx, &store.FindResource{
MemoID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
resourceList := []*Resource{}
for _, resource := range list {
resourceList = append(resourceList, convertResourceFromStore(resource))
}
return c.JSON(http.StatusOK, resourceList)
})
g.DELETE("/memo/:memoId/resource/:resourceId", func(c echo.Context) error { // unbindMemoResource godoc
//
// @Summary Unbind resource from memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to unbind resource from"
// @Param resourceId path int true "ID of resource to unbind from memo"
// @Success 200 {boolean} true "Memo resource unbinded. *200 is returned even if the reference doesn't exists "
// @Failure 400 {object} nil "Memo ID is not a number: %s | Resource ID is not a number: %s | Memo not found"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find memo | Failed to fetch resource list"
// @Security ApiKeyAuth
// @Router /api/v1/memo/{memoId}/resource/{resourceId} [DELETE]
func (s *APIV1Service) unbindMemoResource(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32) userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok { if !ok {
...@@ -131,5 +178,4 @@ func (s *APIV1Service) registerMemoResourceRoutes(g *echo.Group) { ...@@ -131,5 +178,4 @@ func (s *APIV1Service) registerMemoResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
} }
return c.JSON(http.StatusOK, true) return c.JSON(http.StatusOK, true)
})
} }
This diff is collapsed.
...@@ -21,7 +21,19 @@ const maxRSSItemCount = 100 ...@@ -21,7 +21,19 @@ const maxRSSItemCount = 100
const maxRSSItemTitleLength = 100 const maxRSSItemTitleLength = 100
func (s *APIV1Service) registerRSSRoutes(g *echo.Group) { func (s *APIV1Service) registerRSSRoutes(g *echo.Group) {
g.GET("/explore/rss.xml", func(c echo.Context) error { g.GET("/explore/rss.xml", s.getRSS)
g.GET("/u/:id/rss.xml", s.getUserRSS)
}
// getRSS godoc
//
// @Summary Get RSS
// @Tags rss
// @Produce xml
// @Success 200 {object} nil "RSS"
// @Failure 500 {object} nil "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
// @Router /explore/rss.xml [GET]
func (s *APIV1Service) getRSS(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx) systemCustomizedProfile, err := s.getSystemCustomizedProfile(ctx)
if err != nil { if err != nil {
...@@ -45,9 +57,19 @@ func (s *APIV1Service) registerRSSRoutes(g *echo.Group) { ...@@ -45,9 +57,19 @@ func (s *APIV1Service) registerRSSRoutes(g *echo.Group) {
} }
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
return c.String(http.StatusOK, rss) return c.String(http.StatusOK, rss)
}) }
g.GET("/u/:id/rss.xml", func(c echo.Context) error { // getUserRSS godoc
//
// @Summary Get RSS for a user
// @Tags rss
// @Produce xml
// @Param id path int true "User ID"
// @Success 200 {object} nil "RSS"
// @Failure 400 {object} nil "User id is not a number"
// @Failure 500 {object} nil "Failed to get system customized profile | Failed to find memo list | Failed to generate rss"
// @Router /u/{id}/rss.xml [GET]
func (s *APIV1Service) getUserRSS(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
id, err := util.ConvertStringToInt32(c.Param("id")) id, err := util.ConvertStringToInt32(c.Param("id"))
if err != nil { if err != nil {
...@@ -77,7 +99,6 @@ func (s *APIV1Service) registerRSSRoutes(g *echo.Group) { ...@@ -77,7 +99,6 @@ func (s *APIV1Service) registerRSSRoutes(g *echo.Group) {
} }
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8)
return c.String(http.StatusOK, rss) return c.String(http.StatusOK, rss)
})
} }
func (s *APIV1Service) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string, profile *CustomizedProfile) (string, error) { func (s *APIV1Service) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string, profile *CustomizedProfile) (string, error) {
......
This diff is collapsed.
...@@ -43,11 +43,32 @@ type SystemStatus struct { ...@@ -43,11 +43,32 @@ type SystemStatus struct {
} }
func (s *APIV1Service) registerSystemRoutes(g *echo.Group) { func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
g.GET("/ping", func(c echo.Context) error { g.GET("/ping", s.ping)
g.GET("/status", s.status)
g.POST("/system/vacuum", s.vacuum)
}
// ping godoc
//
// @Summary Ping the system
// @Tags system
// @Produce json
// @Success 200 {object} profile.Profile "System profile"
// @Router /api/v1/ping [GET]
func (s *APIV1Service) ping(c echo.Context) error {
return c.JSON(http.StatusOK, s.Profile) return c.JSON(http.StatusOK, s.Profile)
}) }
g.GET("/status", func(c echo.Context) error { // status godoc
//
// @Summary Get system status
// @Tags system
// @Produce json
// @Success 200 {object} SystemStatus "System status"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find host user | Failed to find system setting list | Failed to unmarshal system setting customized profile value"
// @Router /api/v1/status [GET]
func (s *APIV1Service) status(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
systemStatus := SystemStatus{ systemStatus := SystemStatus{
...@@ -133,9 +154,19 @@ func (s *APIV1Service) registerSystemRoutes(g *echo.Group) { ...@@ -133,9 +154,19 @@ func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
} }
return c.JSON(http.StatusOK, systemStatus) return c.JSON(http.StatusOK, systemStatus)
}) }
g.POST("/system/vacuum", func(c echo.Context) error { // vacuum godoc
//
// @Summary Vacuum the database
// @Tags system
// @Produce json
// @Success 200 {boolean} true "Database vacuumed"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find user | Failed to vacuum database"
// @Security ApiKeyAuth
// @Router /api/v1/system/vacuum [POST]
func (s *APIV1Service) vacuum(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32) userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok { if !ok {
...@@ -156,5 +187,4 @@ func (s *APIV1Service) registerSystemRoutes(g *echo.Group) { ...@@ -156,5 +187,4 @@ func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to vacuum database").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to vacuum database").SetInternal(err)
} }
return c.JSON(http.StatusOK, true) return c.JSON(http.StatusOK, true)
})
} }
...@@ -43,6 +43,7 @@ const ( ...@@ -43,6 +43,7 @@ const (
// SystemSettingAutoBackupIntervalName is the name of auto backup interval as seconds. // SystemSettingAutoBackupIntervalName is the name of auto backup interval as seconds.
SystemSettingAutoBackupIntervalName SystemSettingName = "auto-backup-interval" SystemSettingAutoBackupIntervalName SystemSettingName = "auto-backup-interval"
) )
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item. // CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
type CustomizedProfile struct { type CustomizedProfile struct {
...@@ -77,7 +78,113 @@ type UpsertSystemSettingRequest struct { ...@@ -77,7 +78,113 @@ type UpsertSystemSettingRequest struct {
Description string `json:"description"` Description string `json:"description"`
} }
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"` func (s *APIV1Service) registerSystemSettingRoutes(g *echo.Group) {
g.GET("/system/setting", s.getSystemSettingList)
g.POST("/system/setting", s.createSystemSetting)
}
// getSystemSettingList godoc
//
// @Summary Get a list of system settings
// @Tags system-setting
// @Produce json
// @Success 200 {object} []SystemSetting "System setting list"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find user | Failed to find system setting list"
// @Security ApiKeyAuth
// @Router /api/v1/system/setting [GET]
func (s *APIV1Service) getSystemSettingList(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
list, err := s.Store.ListSystemSettings(ctx, &store.FindSystemSetting{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
systemSettingList := make([]*SystemSetting, 0, len(list))
for _, systemSetting := range list {
systemSettingList = append(systemSettingList, convertSystemSettingFromStore(systemSetting))
}
return c.JSON(http.StatusOK, systemSettingList)
}
// createSystemSetting godoc
//
// @Summary Create system setting
// @Tags system-setting
// @Accept json
// @Produce json
// @Param body body UpsertSystemSettingRequest true "Request object."
// @Success 200 {object} store.SystemSetting "Created system setting"
// @Failure 400 {object} nil "Malformatted post system setting request | invalid system setting"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 403 {object} nil "Cannot disable passwords if no SSO identity provider is configured."
// @Failure 500 {object} nil "Failed to find user | Failed to upsert system setting"
// @Security ApiKeyAuth
// @Router /api/v1/system/setting [POST]
func (s *APIV1Service) createSystemSetting(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
systemSettingUpsert := &UpsertSystemSettingRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(systemSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post system setting request").SetInternal(err)
}
if err := systemSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
}
if systemSettingUpsert.Name == SystemSettingDisablePasswordLoginName {
var disablePasswordLogin bool
if err := json.Unmarshal([]byte(systemSettingUpsert.Value), &disablePasswordLogin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
}
identityProviderList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
if disablePasswordLogin && len(identityProviderList) == 0 {
return echo.NewHTTPError(http.StatusForbidden, "Cannot disable passwords if no SSO identity provider is configured.")
}
}
systemSetting, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: systemSettingUpsert.Name.String(),
Value: systemSettingUpsert.Value,
Description: systemSettingUpsert.Description,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
return c.JSON(http.StatusOK, convertSystemSettingFromStore(systemSetting))
}
func (upsert UpsertSystemSettingRequest) Validate() error { func (upsert UpsertSystemSettingRequest) Validate() error {
switch settingName := upsert.Name; settingName { switch settingName := upsert.Name; settingName {
...@@ -172,87 +279,6 @@ func (upsert UpsertSystemSettingRequest) Validate() error { ...@@ -172,87 +279,6 @@ func (upsert UpsertSystemSettingRequest) Validate() error {
return nil return nil
} }
func (s *APIV1Service) registerSystemSettingRoutes(g *echo.Group) {
g.POST("/system/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
systemSettingUpsert := &UpsertSystemSettingRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(systemSettingUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post system setting request").SetInternal(err)
}
if err := systemSettingUpsert.Validate(); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
}
if systemSettingUpsert.Name == SystemSettingDisablePasswordLoginName {
var disablePasswordLogin bool
if err := json.Unmarshal([]byte(systemSettingUpsert.Value), &disablePasswordLogin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid system setting").SetInternal(err)
}
identityProviderList, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
if disablePasswordLogin && len(identityProviderList) == 0 {
return echo.NewHTTPError(http.StatusForbidden, "Cannot disable passwords if no SSO identity provider is configured.")
}
}
systemSetting, err := s.Store.UpsertSystemSetting(ctx, &store.SystemSetting{
Name: systemSettingUpsert.Name.String(),
Value: systemSettingUpsert.Value,
Description: systemSettingUpsert.Description,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert system setting").SetInternal(err)
}
return c.JSON(http.StatusOK, convertSystemSettingFromStore(systemSetting))
})
g.GET("/system/setting", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil || user.Role != store.RoleHost {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
list, err := s.Store.ListSystemSettings(ctx, &store.FindSystemSetting{})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
}
systemSettingList := make([]*SystemSetting, 0, len(list))
for _, systemSetting := range list {
systemSettingList = append(systemSettingList, convertSystemSettingFromStore(systemSetting))
}
return c.JSON(http.StatusOK, systemSettingList)
})
}
func convertSystemSettingFromStore(systemSetting *store.SystemSetting) *SystemSetting { func convertSystemSettingFromStore(systemSetting *store.SystemSetting) *SystemSetting {
return &SystemSetting{ return &SystemSetting{
Name: SystemSettingName(systemSetting.Name), Name: SystemSettingName(systemSetting.Name),
......
...@@ -28,7 +28,57 @@ type DeleteTagRequest struct { ...@@ -28,7 +28,57 @@ type DeleteTagRequest struct {
} }
func (s *APIV1Service) registerTagRoutes(g *echo.Group) { func (s *APIV1Service) registerTagRoutes(g *echo.Group) {
g.POST("/tag", func(c echo.Context) error { g.GET("/tag", s.getTagList)
g.POST("/tag", s.createTag)
g.POST("/tag/delete", s.deleteTag)
g.GET("/tag/suggestion", s.getTagSuggestion)
}
// getTagList godoc
//
// @Summary Get a list of tags
// @Tags tag
// @Produce json
// @Success 200 {object} []string "Tag list"
// @Failure 400 {object} nil "Missing user id to find tag"
// @Failure 500 {object} nil "Failed to find tag list"
// @Security ApiKeyAuth
// @Router /api/v1/tag [GET]
func (s *APIV1Service) getTagList(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag")
}
list, err := s.Store.ListTags(ctx, &store.FindTag{
CreatorID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err)
}
tagNameList := []string{}
for _, tag := range list {
tagNameList = append(tagNameList, tag.Name)
}
return c.JSON(http.StatusOK, tagNameList)
}
// createTag godoc
//
// @Summary Create a tag
// @Tags tag
// @Accept json
// @Produce json
// @Param body body UpsertTagRequest true "Request object."
// @Success 200 {object} string "Created tag name"
// @Failure 400 {object} nil "Malformatted post tag request | Tag name shouldn't be empty"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to upsert tag | Failed to create activity"
// @Security ApiKeyAuth
// @Router /api/v1/tag [POST]
func (s *APIV1Service) createTag(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32) userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok { if !ok {
...@@ -55,30 +105,57 @@ func (s *APIV1Service) registerTagRoutes(g *echo.Group) { ...@@ -55,30 +105,57 @@ func (s *APIV1Service) registerTagRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
} }
return c.JSON(http.StatusOK, tagMessage.Name) return c.JSON(http.StatusOK, tagMessage.Name)
}) }
g.GET("/tag", func(c echo.Context) error { // deleteTag godoc
//
// @Summary Delete a tag
// @Tags tag
// @Accept json
// @Produce json
// @Param body body DeleteTagRequest true "Request object."
// @Success 200 {boolean} true "Tag deleted"
// @Failure 400 {object} nil "Malformatted post tag request | Tag name shouldn't be empty"
// @Failure 401 {object} nil "Missing user in session"
// @Failure 500 {object} nil "Failed to delete tag name: %v"
// @Security ApiKeyAuth
// @Router /api/v1/tag/delete [POST]
func (s *APIV1Service) deleteTag(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32) userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok { if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find tag") return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
} }
list, err := s.Store.ListTags(ctx, &store.FindTag{ tagDelete := &DeleteTagRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(tagDelete); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
}
if tagDelete.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
}
err := s.Store.DeleteTag(ctx, &store.DeleteTag{
Name: tagDelete.Name,
CreatorID: userID, CreatorID: userID,
}) })
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find tag list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagDelete.Name)).SetInternal(err)
}
tagNameList := []string{}
for _, tag := range list {
tagNameList = append(tagNameList, tag.Name)
} }
return c.JSON(http.StatusOK, tagNameList) return c.JSON(http.StatusOK, true)
}) }
g.GET("/tag/suggestion", func(c echo.Context) error { // getTagSuggestion godoc
//
// @Summary Get a list of tags suggested from other memos contents
// @Tags tag
// @Produce json
// @Success 200 {object} []string "Tag list"
// @Failure 400 {object} nil "Missing user session"
// @Failure 500 {object} nil "Failed to find memo list | Failed to find tag list"
// @Security ApiKeyAuth
// @Router /api/v1/tag/suggestion [GET]
func (s *APIV1Service) getTagSuggestion(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32) userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok { if !ok {
...@@ -121,32 +198,6 @@ func (s *APIV1Service) registerTagRoutes(g *echo.Group) { ...@@ -121,32 +198,6 @@ func (s *APIV1Service) registerTagRoutes(g *echo.Group) {
} }
sort.Strings(tagList) sort.Strings(tagList)
return c.JSON(http.StatusOK, tagList) return c.JSON(http.StatusOK, tagList)
})
g.POST("/tag/delete", func(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(auth.UserIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
tagDelete := &DeleteTagRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(tagDelete); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post tag request").SetInternal(err)
}
if tagDelete.Name == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
}
err := s.Store.DeleteTag(ctx, &store.DeleteTag{
Name: tagDelete.Name,
CreatorID: userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete tag name: %v", tagDelete.Name)).SetInternal(err)
}
return c.JSON(http.StatusOK, true)
})
} }
func (s *APIV1Service) createTagCreateActivity(c echo.Context, tag *Tag) error { func (s *APIV1Service) createTagCreateActivity(c echo.Context, tag *Tag) error {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment