package server // go get -u github.com/swaggo/swag/cmd/swag // run: swag init --parseDependency --parseInternal --generalInfo api.go // in the internal/server folder import ( "encoding/json" "net/http" "strings" jsonpatch "github.com/evanphx/json-patch" "github.com/gin-gonic/gin" "github.com/h44z/wg-portal/internal/users" ) // @title WireGuard Portal API // @version 1.0 // @description WireGuard Portal API for managing users and peers. // @license.name MIT // @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt // @securityDefinitions.basic ApiBasicAuth // @in header // @name Authorization // @BasePath /api/v1 // ApiServer is a simple wrapper struct so that we can have fresh member function names. type ApiServer struct { s *Server } type ApiError struct { Message string } // GetUsers godoc // @Summary Retrieves all users // @Produce json // @Success 200 {object} []users.User // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Router /users [get] // @Security ApiBasicAuth func (s *ApiServer) GetUsers(c *gin.Context) { allUsers := s.s.users.GetUsersUnscoped() for i := range allUsers { allUsers[i].Password = "" // do not publish password... } c.JSON(http.StatusOK, allUsers) } // GetUser godoc // @Summary Retrieves user based on given Email // @Produce json // @Param email path string true "User Email" // @Success 200 {object} users.User // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Router /user/{email} [get] // @Security ApiBasicAuth func (s *ApiServer) GetUser(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.Param("email"))) if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } user := s.s.users.GetUserUnscoped(c.Param("email")) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user not found"}) return } user.Password = "" // do not send password... c.JSON(http.StatusOK, user) } // PostUser godoc // @Summary Creates a new user based on the given user model // @Produce json // @Success 200 {object} users.User // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Failure 500 {object} ApiError // @Router /users [post] // @Security ApiBasicAuth func (s *ApiServer) PostUser(c *gin.Context) { newUser := users.User{} if err := c.BindJSON(&newUser); err != nil { c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) return } if user := s.s.users.GetUserUnscoped(newUser.Email); user != nil { c.JSON(http.StatusBadRequest, ApiError{Message: "user already exists"}) return } if err := s.s.CreateUser(newUser, s.s.wg.Cfg.GetDefaultDeviceName()); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } user := s.s.users.GetUserUnscoped(newUser.Email) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user not found"}) return } user.Password = "" // do not send password... c.JSON(http.StatusOK, user) } // PutUser godoc // @Summary Updates a user based on the given user model // @Produce json // @Param email path string true "User Email" // @Success 200 {object} users.User // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Failure 500 {object} ApiError // @Router /user/{email} [put] // @Security ApiBasicAuth func (s *ApiServer) PutUser(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.Param("email"))) if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } updateUser := users.User{} if err := c.BindJSON(&updateUser); err != nil { c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) return } // Changing email address is not allowed if email != updateUser.Email { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"}) return } if user := s.s.users.GetUserUnscoped(email); user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"}) return } if err := s.s.UpdateUser(updateUser); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } user := s.s.users.GetUserUnscoped(email) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user not found"}) return } user.Password = "" // do not send password... c.JSON(http.StatusOK, user) } // PatchUser godoc // @Summary Updates a user based on the given partial user model // @Produce json // @Param email path string true "User Email" // @Success 200 {object} users.User // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Failure 500 {object} ApiError // @Router /user/{email} [patch] // @Security ApiBasicAuth func (s *ApiServer) PatchUser(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.Param("email"))) if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } patch, err := c.GetRawData() if err != nil { c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()}) return } user := s.s.users.GetUserUnscoped(email) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"}) return } userData, err := json.Marshal(user) if err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } mergedUserData, err := jsonpatch.MergePatch(userData, patch) var mergedUser users.User err = json.Unmarshal(mergedUserData, &mergedUser) if err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } // CHanging email address is not allowed if email != mergedUser.Email { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"}) return } if err := s.s.UpdateUser(mergedUser); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } user = s.s.users.GetUserUnscoped(email) if user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user not found"}) return } user.Password = "" // do not send password... c.JSON(http.StatusOK, user) } // DeleteUser godoc // @Summary Deletes the specified user // @Produce json // @Param email path string true "User Email" // @Success 204 "No content" // @Failure 400 {object} ApiError // @Failure 401 {object} ApiError // @Failure 403 {object} ApiError // @Failure 404 {object} ApiError // @Failure 500 {object} ApiError // @Router /user/{email} [delete] // @Security ApiBasicAuth func (s *ApiServer) DeleteUser(c *gin.Context) { email := strings.ToLower(strings.TrimSpace(c.Param("email"))) if email == "" { c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"}) return } var user *users.User if user = s.s.users.GetUserUnscoped(email); user == nil { c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"}) return } if err := s.s.DeleteUser(*user); err != nil { c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()}) return } c.Status(http.StatusNoContent) }