diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..50a63d6 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,59 @@ +name: Documentation + +on: + push: + paths-ignore: + - '**.md' + - '**.jpg' + - '**.jpeg' + - '**.png' + - '**.yaml' + - '**.json' + +jobs: + build_documentation: + name: Build documentation + runs-on: ubuntu-latest + permissions: + contents: read + if: ${{ github.ref == 'refs/heads/main' }} + steps: + - name: Checkout the repository + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Install swag + run: go install github.com/swaggo/swag/cmd/swag + + - name: Install redoc + run: sudo apt install npm && sudo npm install redoc + + - name: Build the API documentation + run: /home/runner/go/bin/swag init --parseDependency=true -d . -g cmd/pushbits/main.go + + - name: Build static HTML + run: npx redoc-cli bundle docs/swagger.yaml --output index.html + + - name: Setup SSH keys and known_hosts + env: + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + run: | + ssh-agent -a $SSH_AUTH_SOCK > /dev/null + ssh-add - <<< "${{ secrets.WEBSITE_DEPLOY_KEY }}" + + - name: Checkout website + env: + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + run: mkdir website && git clone git@github.com:pushbits/website.git website + + - name: Copy index.html + run: cp index.html website/static/api/index.html + + - name: Set Git config + run: git config --global user.email "pipeline@pushbits.io" && git config --global user.name "PushBits Pipeline" + + - name: Commit and push + env: + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + run: cd website && git add . && git commit -m "Update documentation to ${{ github.sha }}" && git push diff --git a/.gitignore b/.gitignore index f5f8b98..63ad616 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ config.yml # vendor/ *.code-workspace + +# Build documentation + +docs/ \ No newline at end of file diff --git a/cmd/pushbits/main.go b/cmd/pushbits/main.go index f2965e6..8de25e1 100644 --- a/cmd/pushbits/main.go +++ b/cmd/pushbits/main.go @@ -26,6 +26,21 @@ func setupCleanup(db *database.Database, dp *dispatcher.Dispatcher) { }() } +// @title PushBits Server API Documentation +// @version 0.7.2 +// @description Documentation for the PushBits server API.
[Contact](https://www.eiken.dev/) + +// @contact.name PushBits +// @contact.url https://github.com/pushbits + +// @license.name ISC +// @license.url https://github.com/pushbits/server/blob/master/LICENSE + +// @host your-domain.net +// @BasePath / +// @query.collection.format multi + +// @securityDefinitions.basic BasicAuth func main() { log.Println("Starting PushBits.") diff --git a/internal/api/application.go b/internal/api/application.go index f90c9bd..9914924 100644 --- a/internal/api/application.go +++ b/internal/api/application.go @@ -109,7 +109,19 @@ func (h *ApplicationHandler) updateApplication(ctx *gin.Context, a *model.Applic return nil } -// CreateApplication creates an application. +// CreateApplication godoc +// @Summary Create Application +// @Description Create a new application +// @ID post-application +// @Tags Application +// @Accept json,mpfd +// @Produce json +// @Param name query string true "Name of the application" +// @Param strict_compatability query boolean false "Use strict compatability mode" +// @Success 200 {object} model.Application +// @Failure 400 "" +// @Security BasicAuth +// @Router /application [post] func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) { var createApplication model.CreateApplication @@ -131,7 +143,17 @@ func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) { ctx.JSON(http.StatusOK, &application) } -// GetApplications returns all applications of the current user. +// GetApplications godoc +// @Summary Get Applications +// @Description Get all applications from current user +// @ID get-application +// @Tags Application +// @Accept json,mpfd +// @Produce json +// @Success 200 {array} model.Application +// @Failure 500 "" +// @Security BasicAuth +// @Router /application [get] func (h *ApplicationHandler) GetApplications(ctx *gin.Context) { user := authentication.GetUser(ctx) if user == nil { @@ -146,7 +168,18 @@ func (h *ApplicationHandler) GetApplications(ctx *gin.Context) { ctx.JSON(http.StatusOK, &applications) } -// GetApplication returns the application with the specified ID. +// GetApplication godoc +// @Summary Get Application +// @Description Get single application by ID +// @ID get-application-id +// @Tags Application +// @Accept json,mpfd +// @Produce json +// @Param id path int true "ID of the application" +// @Success 200 {object} model.Application +// @Failure 404,403 "" +// @Security BasicAuth +// @Router /application/{id} [get] func (h *ApplicationHandler) GetApplication(ctx *gin.Context) { application, err := getApplication(ctx, h.DB) if err != nil { @@ -167,7 +200,18 @@ func (h *ApplicationHandler) GetApplication(ctx *gin.Context) { ctx.JSON(http.StatusOK, &application) } -// DeleteApplication deletes an application with a certain ID. +// DeleteApplication godoc +// @Summary Delete Application +// @Description Delete an application +// @ID delete-application-id +// @Tags Application +// @Accept json,mpfd +// @Produce json +// @Param id path int true "ID of the application" +// @Success 200 "" +// @Failure 500,404,403 "" +// @Security BasicAuth +// @Router /application/{id} [delete] func (h *ApplicationHandler) DeleteApplication(ctx *gin.Context) { application, err := getApplication(ctx, h.DB) if err != nil { @@ -185,7 +229,21 @@ func (h *ApplicationHandler) DeleteApplication(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{}) } -// UpdateApplication updates an application with a certain ID. +// UpdateApplication godoc +// @Summary Update Application +// @Description Update an application +// @ID put-application-id +// @Tags Application +// @Accept json,mpfd +// @Produce json +// @Param id path int true "ID of the application" +// @Param name query string false "New name for the application" +// @Param refresh_token query bool false "Generate new refresh token for the application" +// @Param strict_compatability query bool false "Whether to use strict compataibility mode" +// @Success 200 "" +// @Failure 500,404,403 "" +// @Security BasicAuth +// @Router /application/{id} [put] func (h *ApplicationHandler) UpdateApplication(ctx *gin.Context) { application, err := getApplication(ctx, h.DB) if err != nil { diff --git a/internal/api/health.go b/internal/api/health.go index 7c43159..b9b42ad 100644 --- a/internal/api/health.go +++ b/internal/api/health.go @@ -11,7 +11,15 @@ type HealthHandler struct { DB Database } -// Health returns the health status of the server. +// Health godoc +// @Summary Health of the application +// @ID get-health +// @Tags Health +// @Accept json,mpfd +// @Produce json +// @Success 200 "" +// @Failure 500 "" +// @Router /health [get] func (h *HealthHandler) Health(ctx *gin.Context) { if err := h.DB.Health(); err != nil { ctx.AbortWithError(http.StatusInternalServerError, err) diff --git a/internal/api/notification.go b/internal/api/notification.go index 064c716..c04df4d 100644 --- a/internal/api/notification.go +++ b/internal/api/notification.go @@ -29,7 +29,21 @@ type NotificationHandler struct { DP NotificationDispatcher } -// CreateNotification is used to create a new notification for a user. +// CreateNotification godoc +// @Summary Create a Notification +// @Description Creates a new notification for the given channel +// @ID post-message +// @Tags Application +// @Accept json,mpfd +// @Produce json +// @Param message query string true "The message to send" +// @Param title query string false "The title to send" +// @Param priority query integer false "The notifications priority" +// @Param extras query model.NotificationExtras false "JSON object with additional information" +// @Param token query string true "Channels token, can also be provieded in the header" +// @Success 200 {object} model.Notification +// @Failure 500,404,403 "" +// @Router /message [post] func (h *NotificationHandler) CreateNotification(ctx *gin.Context) { var notification model.Notification @@ -58,7 +72,18 @@ func (h *NotificationHandler) CreateNotification(ctx *gin.Context) { ctx.JSON(http.StatusOK, ¬ification) } -// DeleteNotification is used to delete (or mark as deleted) a notification for a user +// DeleteNotification godoc +// @Summary Delete a Notification +// @Description Informs the channel that the notification is deleted +// @ID deƶete-message-id +// @Tags Application +// @Accept json,mpfd +// @Produce json +// @Param message_id path string true "ID of the message to delete" +// @Param token query string true "Channels token, can also be provieded in the header" +// @Success 200 "" +// @Failure 500,404,403 "" +// @Router /message/{message_id} [DELETE] func (h *NotificationHandler) DeleteNotification(ctx *gin.Context) { application := authentication.GetApplication(ctx) id, err := getMessageID(ctx) diff --git a/internal/api/user.go b/internal/api/user.go index 65e5159..385c443 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -112,8 +112,22 @@ func (h *UserHandler) updateUser(ctx *gin.Context, u *model.User, updateUser mod return nil } -// CreateUser creates a new user. +// CreateUser godoc // This method assumes that the requesting user has privileges. +// @Summary Create a User +// @Description Creates a new user +// @ID post-user +// @Tags User +// @Accept json,mpfd +// @Produce json +// @Param name query string true "Name of the user" +// @Param is_admin query bool false "Whether to set the user as admin or not" +// @Param matrix_id query string true "Matrix ID of the user in the format @user:domain.tld" +// @Param password query string true "The users password" +// @Success 200 {object} model.ExternalUser +// @Failure 500,404,403 "" +// @Security BasicAuth +// @Router /user [post] func (h *UserHandler) CreateUser(ctx *gin.Context) { var createUser model.CreateUser @@ -137,8 +151,18 @@ func (h *UserHandler) CreateUser(ctx *gin.Context) { ctx.JSON(http.StatusOK, user.IntoExternalUser()) } -// GetUsers returns all users. +// GetUsers godoc // This method assumes that the requesting user has privileges. +// @Summary Get Users +// @Description Gets a list of all users +// @ID get-user +// @Tags User +// @Accept json,mpfd +// @Produce json +// @Success 200 {object} []model.ExternalUser +// @Failure 500 "" +// @Security BasicAuth +// @Router /user [get] func (h *UserHandler) GetUsers(ctx *gin.Context) { users, err := h.DB.GetUsers() if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success { @@ -154,8 +178,19 @@ func (h *UserHandler) GetUsers(ctx *gin.Context) { ctx.JSON(http.StatusOK, &externalUsers) } -// GetUser returns the user with the specified ID. +// GetUser godoc // This method assumes that the requesting user has privileges. +// @Summary Get User +// @Description Gets single user +// @ID get-user-id +// @Tags User +// @Accept json,mpfd +// @Produce json +// @Param id path integer true "The users id" +// @Success 200 {object} model.ExternalUser +// @Failure 500,404 "" +// @Security BasicAuth +// @Router /user/{id} [get] func (h *UserHandler) GetUser(ctx *gin.Context) { user, err := getUser(ctx, h.DB) if err != nil { @@ -165,9 +200,19 @@ func (h *UserHandler) GetUser(ctx *gin.Context) { ctx.JSON(http.StatusOK, user.IntoExternalUser()) } -// DeleteUser deletes a user with a certain ID. -// +// DeleteUser godoc // This method assumes that the requesting user has privileges. +// @Summary Delete User +// @Description Delete user +// @ID delete-user-id +// @Tags User +// @Accept json,mpfd +// @Produce json +// @Param id path integer true "The users id" +// @Success 200 "" +// @Failure 500,404 "" +// @Security BasicAuth +// @Router /user/{id} [delete] func (h *UserHandler) DeleteUser(ctx *gin.Context) { user, err := getUser(ctx, h.DB) if err != nil { @@ -194,10 +239,24 @@ func (h *UserHandler) DeleteUser(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{}) } -// UpdateUser updates a user with a certain ID. -// +// UpdateUser godoc // This method assumes that the requesting user has privileges. If users can later update their own user, make sure they // cannot give themselves privileges. +// @Summary Update User +// @Description Update user information +// @ID put-user-id +// @Tags User +// @Accept json,mpfd +// @Produce json +// @Param id path integer true "The users id" +// @Param name query string true "Name of the user" +// @Param is_admin query bool false "Whether to set the user as admin or not" +// @Param matrix_id query string true "Matrix ID of the user in the format @user:domain.tld" +// @Param password query string true "The users password" +// @Success 200 "" +// @Failure 500,404,400 "" +// @Security BasicAuth +// @Router /user/{id} [put] func (h *UserHandler) UpdateUser(ctx *gin.Context) { user, err := getUser(ctx, h.DB) if err != nil { diff --git a/internal/model/notification.go b/internal/model/notification.go index fc46221..6239236 100644 --- a/internal/model/notification.go +++ b/internal/model/notification.go @@ -21,3 +21,6 @@ type DeleteNotification struct { ID string `json:"id" form:"id"` Date time.Time `json:"date"` } + +// NotificationExtras is need to document Notification.Extras in a format that the tool can read. +type NotificationExtras map[string]interface{}