diff --git a/api/v1/auth.go b/api/v1/auth.go index 62828eb3..c42e5ab8 100644 --- a/api/v1/auth.go +++ b/api/v1/auth.go @@ -33,6 +33,7 @@ type SignUp struct { } func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { + // POST /auth/signin - Sign in. g.POST("/auth/signin", func(c echo.Context) error { ctx := c.Request().Context() signin := &SignIn{} @@ -67,6 +68,7 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { return c.JSON(http.StatusOK, user) }) + // POST /auth/signin/sso - Sign in with SSO g.POST("/auth/signin/sso", func(c echo.Context) error { ctx := c.Request().Context() signin := &SSOSignIn{} @@ -153,6 +155,7 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { return c.JSON(http.StatusOK, user) }) + // POST /auth/signup - Sign up a new user. g.POST("/auth/signup", func(c echo.Context) error { ctx := c.Request().Context() signup := &SignUp{} @@ -218,6 +221,7 @@ func (s *APIV1Service) registerAuthRoutes(g *echo.Group) { return c.JSON(http.StatusOK, user) }) + // POST /auth/signout - Sign out. g.POST("/auth/signout", func(c echo.Context) error { auth.RemoveTokensAndCookies(c) return c.JSON(http.StatusOK, true) diff --git a/api/v1/http_getter.go b/api/v1/http_getter.go index 6d24887c..f98b0fda 100644 --- a/api/v1/http_getter.go +++ b/api/v1/http_getter.go @@ -10,6 +10,7 @@ import ( ) func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) { + // GET /get/httpmeta?url={url} - Get website meta. g.GET("/get/httpmeta", func(c echo.Context) error { urlStr := c.QueryParam("url") if urlStr == "" { @@ -26,6 +27,7 @@ func (*APIV1Service) registerGetterPublicRoutes(g *echo.Group) { return c.JSON(http.StatusOK, htmlMeta) }) + // GET /get/image?url={url} - Get image. g.GET("/get/image", func(c echo.Context) error { urlStr := c.QueryParam("url") if urlStr == "" { diff --git a/api/v1/user.go b/api/v1/user.go index bb04e090..50ddc4b0 100644 --- a/api/v1/user.go +++ b/api/v1/user.go @@ -129,6 +129,7 @@ func (update UpdateUserRequest) Validate() error { } func (s *APIV1Service) registerUserRoutes(g *echo.Group) { + // POST /user - Create a new user. g.POST("/user", func(c echo.Context) error { ctx := c.Request().Context() userID, ok := c.Get(getUserIDContextKey()).(int) @@ -184,6 +185,7 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { return c.JSON(http.StatusOK, userMessage) }) + // GET /user - List all users. g.GET("/user", func(c echo.Context) error { ctx := c.Request().Context() list, err := s.Store.ListUsers(ctx, &store.FindUser{}) @@ -202,7 +204,7 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { return c.JSON(http.StatusOK, userMessageList) }) - // GET /api/user/me is used to check if the user is logged in. + // GET /user/me - Get current user. g.GET("/user/me", func(c echo.Context) error { ctx := c.Request().Context() userID, ok := c.Get(getUserIDContextKey()).(int) @@ -233,6 +235,7 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { return c.JSON(http.StatusOK, userMessage) }) + // GET /user/:id - Get user by id. g.GET("/user/:id", func(c echo.Context) error { ctx := c.Request().Context() id, err := strconv.Atoi(c.Param("id")) @@ -255,6 +258,7 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { return c.JSON(http.StatusOK, userMessage) }) + // PUT /user/:id - Update user by id. g.PATCH("/user/:id", func(c echo.Context) error { ctx := c.Request().Context() userID, err := strconv.Atoi(c.Param("id")) @@ -339,6 +343,7 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) { return c.JSON(http.StatusOK, userMessage) }) + // DELETE /user/:id - Delete user by id. g.DELETE("/user/:id", func(c echo.Context) error { ctx := c.Request().Context() currentUserID, ok := c.Get(getUserIDContextKey()).(int) diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 00000000..179db4ca --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,107 @@ +# Authentication APIs + +## Sign In + +``` +POST /api/v1/auth/signin +``` + +**Request Body** + +```json +{ + "username": "john", + "password": "password123" +} +``` + +**Response** + +```json +{ + "id": 123, + "username": "john", + "nickname": "John" + // other user fields +} +``` + +**Status Codes** + +- 200: Sign in success +- 400: Invalid request +- 401: Incorrect credentials +- 403: User banned +- 500: Internal server error + +## SSO Sign In + +``` +POST /api/v1/auth/signin/sso +``` + +**Request Body** + +```json +{ + "identityProviderId": 123, + "code": "abc123", + "redirectUri": "https://example.com/callback" +} +``` + +**Response** + +Same as **Sign In** + +**Status Codes** + +- 200: Success +- 400: Invalid request +- 401: Authentication failed +- 403: User banned +- 404: Identity provider not found +- 500: Internal server error + +## Sign Up + +``` +POST /api/v1/auth/signup +``` + +**Request Body** + +```json +{ + "username": "mary", + "password": "password456" +} +``` + +**Response** + +Same as **Sign In** + +**Status Codes** + +- 200: Sign up success +- 400: Invalid request +- 401: Sign up disabled +- 500: Internal server error + +## Sign Out + +``` +POST /api/v1/auth/signout +``` + +**Response** + +``` +true +``` + +**Status Codes** + +- 200: Success +- 500: Internal server error diff --git a/docs/api/how-to.md b/docs/api/how-to.md new file mode 100644 index 00000000..ebc36283 --- /dev/null +++ b/docs/api/how-to.md @@ -0,0 +1,44 @@ +# Guide to Access Memos API with OpenID + +Memos API supports using OpenID as the user identifier to access the API. + +## What is OpenID + +OpenID is a unique identifier assigned by Memos system to each user. + +When a user registers or logs in via third-party OAuth through Memos system, the OpenID will be generated automatically. + +## How to Get User's OpenID + +You can get a user's OpenID through: + +- User checks the personal profile page in Memos system +- Calling Memos API to get user details +- Retrieving from login API response after successful login + +Example: + +``` +// GET /api/v1/user/me + +{ + "id": 123, + "username": "john", + "openId": "8613E04B4FA6603883F05A5E0A5E2517", + ... +} +``` + +## How to Use OpenID to Access API + +You can access the API on behalf of the user by appending `?openId=xxx` parameter to the API URL. + +For example: + +``` +curl 'https://demo.usememos.com/api/v1/memo?openId=8613E04B4FA6603883F05A5E0A5E2517' -H 'Content-Type: application/json' --data-raw '{"content":"Hello world!"}' +``` + +The above request will create a Memo under the user with OpenID `8613E04B4FA6603883F05A5E0A5E2517`. + +OpenID can be used in any API that requires user identity. diff --git a/docs/api/memo-relation.md b/docs/api/memo-relation.md new file mode 100644 index 00000000..ff53882e --- /dev/null +++ b/docs/api/memo-relation.md @@ -0,0 +1,67 @@ +# Memo Relation APIs + +## Create Memo Relation + +``` +POST /api/v1/memo/:memoId/relation +``` + +**Request Body** + +```json +{ + "relatedMemoId": 456, + "type": "REFERENCE" +} +``` + +**Response** + +```json +{ + "memoId": 123, + "relatedMemoId": 456, + "type": "REFERENCE" +} +``` + +**Status Codes** + +- 200: OK +- 400: Invalid request +- 500: Internal server error + +## Get Memo Relations + +``` +GET /api/v1/memo/:memoId/relation +``` + +**Response** + +```json +[ + { + "memoId": 123, + "relatedMemoId": 456, + "type": "REFERENCE" + } +] +``` + +**Status Codes** + +- 200: OK +- 500: Internal server error + +## Delete Memo Relation + +``` +DELETE /api/v1/memo/:memoId/relation/:relatedMemoId/type/:relationType +``` + +**Status Codes** + +- 200: Deleted +- 400: Invalid request +- 500: Internal server error diff --git a/docs/api/memo-resource.md b/docs/api/memo-resource.md new file mode 100644 index 00000000..819c57cf --- /dev/null +++ b/docs/api/memo-resource.md @@ -0,0 +1,65 @@ +# Memo Resource APIs + +## Bind Resource to Memo + +``` +POST /api/v1/memo/:memoId/resource +``` + +**Request Body** + +```json +{ + "resourceId": 123 +} +``` + +**Response** + +``` +true +``` + +**Status Codes** + +- 200: OK +- 400: Invalid request +- 401: Unauthorized +- 404: Memo/Resource not found +- 500: Internal server error + +## Get Memo Resources + +``` +GET /api/v1/memo/:memoId/resource +``` + +**Response** + +```json +[ + { + "id": 123, + "filename": "example.png" + // other resource fields + } +] +``` + +**Status Codes** + +- 200: OK +- 500: Internal server error + +## Unbind Resource from Memo + +``` +DELETE /api/v1/memo/:memoId/resource/:resourceId +``` + +**Status Codes** + +- 200: OK +- 401: Unauthorized +- 404: Memo/Resource not found +- 500: Internal server error diff --git a/docs/api/memo.md b/docs/api/memo.md new file mode 100644 index 00000000..b54d22f0 --- /dev/null +++ b/docs/api/memo.md @@ -0,0 +1,135 @@ +# Memo APIs + +## Create Memo + +``` +POST /api/v1/memo +``` + +**Request Body** + +```json +{ + "content": "Memo content", + "visibility": "PUBLIC", + "resourceIdList": [123, 456], + "relationList": [{ "relatedMemoId": 789, "type": "LINK" }] +} +``` + +**Response** + +```json +{ + "id": 1234, + "content": "Memo content", + "visibility": "PUBLIC" + // other fields +} +``` + +**Status Codes** + +- 200: Created +- 400: Invalid request +- 401: Unauthorized +- 403: Forbidden to create public memo +- 500: Internal server error + +## Get Memo List + +``` +GET /api/v1/memo +``` + +**Parameters** + +- `creatorId` (optional): Filter by creator ID +- `visibility` (optional): Filter visibility, `PUBLIC`, `PROTECTED` or `PRIVATE` +- `pinned` (optional): Filter pinned memo, `true` or `false` +- `tag` (optional): Filter memo with tag +- `content` (optional): Search in content +- `limit` (optional): Limit number of results +- `offset` (optional): Offset of first result + +**Response** + +```json +[ + { + "id": 1234, + "content": "Memo 1" + // other fields + }, + { + "id": 5678, + "content": "Memo 2" + // other fields + } +] +``` + +## Get Memo By ID + +``` +GET /api/v1/memo/:memoId +``` + +**Response** + +```json +{ + "id": 1234, + "content": "Memo content" + // other fields +} +``` + +**Status Codes** + +- 200: Success +- 403: Forbidden for private memo +- 404: Not found +- 500: Internal server error + +## Update Memo + +``` +PATCH /api/v1/memo/:memoId +``` + +**Request Body** + +```json +{ + "content": "Updated content", + "visibility": "PRIVATE" +} +``` + +**Response** + +Same as **Get Memo By ID** + +**Status Codes** + +- 200: Updated +- 400: Invalid request +- 401: Unauthorized +- 403: Forbidden +- 404: Not found +- 500: Internal server error + +## Delete Memo + +``` +DELETE /api/v1/memo/:memoId +``` + +**Status Codes** + +- 200: Deleted +- 401: Unauthorized +- 403: Forbidden +- 404: Not found +- 500: Internal server error diff --git a/docs/api/resource.md b/docs/api/resource.md new file mode 100644 index 00000000..5d09c59c --- /dev/null +++ b/docs/api/resource.md @@ -0,0 +1,130 @@ +# Resource APIs + +## Upload Resource + +### Upload File + +``` +POST /api/v1/resource/blob +``` + +**Request Form** + +- `file`: Upload file + +**Response** + +```json +{ + "id": 123, + "filename": "example.png" + // other fields +} +``` + +**Status Codes** + +- 200: OK +- 400: Invalid request +- 401: Unauthorized +- 413: File too large +- 500: Internal server error + +### Create Resource + +``` +POST /api/v1/resource +``` + +**Request Body** + +```json +{ + "filename": "example.png", + "externalLink": "https://example.com/image.png" +} +``` + +**Response** + +Same as **Upload File** + +**Status Codes** + +- 200: OK +- 400: Invalid request +- 401: Unauthorized +- 500: Internal server error + +## Get Resource List + +``` +GET /api/v1/resource +``` + +**Parameters** + +- `limit` (optional): Limit number of results +- `offset` (optional): Offset of first result + +**Response** + +```json +[ + { + "id": 123, + "filename": "example.png" + // other fields + }, + { + "id": 456, + "filename": "doc.pdf" + // other fields + } +] +``` + +**Status Codes** + +- 200: OK +- 401: Unauthorized +- 500: Internal server error + +## Update Resource + +``` +PATCH /api/v1/resource/:resourceId +``` + +**Request Body** + +```json +{ + "filename": "new_name.png" +} +``` + +**Response** + +Same as **Get Resource List** + +**Status Codes** + +- 200: OK +- 400: Invalid request +- 401: Unauthorized +- 404: Not found +- 500: Internal server error + +## Delete Resource + +``` +DELETE /api/v1/resource/:resourceId +``` + +**Status Codes** + +- 200: Deleted +- 401: Unauthorized +- 404: Not found +- 500: Internal server error diff --git a/docs/api/tag.md b/docs/api/tag.md new file mode 100644 index 00000000..4a376193 --- /dev/null +++ b/docs/api/tag.md @@ -0,0 +1,84 @@ +# Tag APIs + +## Create Tag + +``` +POST /api/v1/tag +``` + +**Request Body** + +```json +{ + "name": "python" +} +``` + +**Response** + +``` +"python" +``` + +**Status Codes** + +- 200: Created +- 400: Invalid request +- 500: Internal server error + +## Get Tag List + +``` +GET /api/v1/tag +``` + +**Response** + +```json +["python", "golang", "javascript"] +``` + +**Status Codes** + +- 200: OK +- 401: Unauthorized +- 500: Internal server error + +## Suggest Tags + +``` +GET /api/v1/tag/suggestion +``` + +**Response** + +```json +["django", "flask", "numpy"] +``` + +**Status Codes** + +- 200: OK +- 401: Unauthorized +- 500: Internal server error + +## Delete Tag + +``` +POST /api/v1/tag/delete +``` + +**Request Body** + +```json +{ + "name": "outdated_tag" +} +``` + +**Status Codes** + +- 200: Deleted +- 400: Invalid request +- 401: Unauthorized +- 500: Internal server error diff --git a/docs/api/user.md b/docs/api/user.md new file mode 100644 index 00000000..768f8ba6 --- /dev/null +++ b/docs/api/user.md @@ -0,0 +1,164 @@ +# User APIs + +## Create User + +``` +POST /api/v1/user +``` + +**Request Body** + +```json +{ + "username": "john", + "role": "USER", + "email": "john@example.com", + "nickname": "John", + "password": "password123" +} +``` + +**Response** + +```json +{ + "id": 123, + "username": "john", + "role": "USER", + "email": "john@example.com", + "nickname": "John", + "avatarUrl": "", + "createdTs": 1596647800, + "updatedTs": 1596647800 +} +``` + +**Status Codes** + +- 200: Success +- 400: Validation error +- 401: Unauthorized +- 403: Forbidden to create host user +- 500: Internal server error + +## Get User List + +``` +GET /api/v1/user +``` + +**Response** + +```json +[ + { + "id": 123, + "username": "john", + "role": "USER" + // other fields + }, + { + "id": 456, + "username": "mary", + "role": "ADMIN" + // other fields + } +] +``` + +**Status Codes** + +- 200: Success +- 500: Internal server error + +## Get User By ID + +``` +GET /api/v1/user/:id +``` + +**Response** + +```json +{ + "id": 123, + "username": "john", + "role": "USER" + // other fields +} +``` + +**Status Codes** + +- 200: Success +- 404: Not found +- 500: Internal server error + +## Update User + +``` +PATCH /api/v1/user/:id +``` + +**Request Body** + +```json +{ + "username": "johnny", + "email": "johnny@example.com", + "nickname": "Johnny", + "avatarUrl": "https://avatars.example.com/u=123" +} +``` + +**Response** + +```json +{ + "id": 123, + "username": "johnny", + "role": "USER", + "email": "johnny@example.com", + "nickname": "Johnny", + "avatarUrl": "https://avatars.example.com/u=123", + "createdTs": 1596647800, + "updatedTs": 1596647900 +} +``` + +**Status Codes** + +- 200: Success +- 400: Validation error +- 403: Forbidden +- 404: Not found +- 500: Internal server error + +## Delete User + +``` +DELETE /api/v1/user/:id +``` + +**Status Codes** + +- 200: Success +- 403: Forbidden +- 404: Not found +- 500: Internal server error + +## Get Current User + +``` +GET /api/v1/user/me +``` + +**Response** + +Same as **Get User By ID** + +**Status Codes** + +- 200: Success +- 401: Unauthorized +- 500: Internal server error