mirror of
https://github.com/h44z/wg-portal
synced 2025-02-26 05:49:14 +00:00
WIP: RESTful API for WireGuard Portal, user endpoint (#11)
This commit is contained in:
parent
b6d9814021
commit
35513ae994
2
.gitignore
vendored
2
.gitignore
vendored
@ -32,3 +32,5 @@ ssh.key
|
||||
.testCoverage.txt
|
||||
wg_portal.db
|
||||
go.sum
|
||||
swagger.json
|
||||
swagger.yaml
|
4
Makefile
4
Makefile
@ -51,6 +51,10 @@ docker-build:
|
||||
docker-push:
|
||||
docker push $(IMAGE)
|
||||
|
||||
api-docs:
|
||||
cd internal/server; swag init --parseDependency --parseInternal --generalInfo api.go
|
||||
$(GOCMD) fmt internal/server/docs/docs.go
|
||||
|
||||
$(BUILDDIR)/%-amd64: cmd/%/main.go dep phony
|
||||
GOOS=linux GOARCH=amd64 $(GOCMD) build -ldflags "-X github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}" -o $@ $<
|
||||
|
||||
|
10
go.mod
10
go.mod
@ -4,21 +4,31 @@ go 1.16
|
||||
|
||||
require (
|
||||
git.prolicht.digital/pub/healthcheck v1.0.1
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/evanphx/json-patch v0.5.2
|
||||
github.com/gin-contrib/sessions v0.0.3
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-ldap/ldap/v3 v3.2.4
|
||||
github.com/go-openapi/spec v0.20.3 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/milosgajdos/tenus v0.0.3
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.7.0
|
||||
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
|
||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
|
||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6 // indirect
|
||||
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 // indirect
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200609130330-bd2cb7843e1b
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gorm.io/driver/mysql v1.0.5
|
||||
|
265
internal/server/api.go
Normal file
265
internal/server/api.go
Normal file
@ -0,0 +1,265 @@
|
||||
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)
|
||||
}
|
466
internal/server/docs/docs.go
Normal file
466
internal/server/docs/docs.go
Normal file
@ -0,0 +1,466 @@
|
||||
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
// This file was generated by swaggo/swag
|
||||
|
||||
package docs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
"github.com/swaggo/swag"
|
||||
)
|
||||
|
||||
var doc = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{.Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"license": {
|
||||
"name": "MIT",
|
||||
"url": "https://github.com/h44z/wg-portal/blob/master/LICENSE.txt"
|
||||
},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/user/{email}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiBasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Retrieves user based on given Email",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User Email",
|
||||
"name": "email",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/users.User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiBasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Updates a user based on the given user model",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User Email",
|
||||
"name": "email",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/users.User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiBasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Deletes the specified user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User Email",
|
||||
"name": "email",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"ApiBasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Updates a user based on the given partial user model",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User Email",
|
||||
"name": "email",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/users.User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiBasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Retrieves all users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/users.User"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiBasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"summary": "Creates a new user based on the given user model",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/users.User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/server.ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"gorm.DeletedAt": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string"
|
||||
},
|
||||
"valid": {
|
||||
"description": "Valid is true if Time is not NULL",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server.ApiError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"users.User": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"firstname",
|
||||
"lastname"
|
||||
],
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"description": "database internal fields",
|
||||
"type": "string"
|
||||
},
|
||||
"deletedAt": {
|
||||
"$ref": "#/definitions/gorm.DeletedAt"
|
||||
},
|
||||
"email": {
|
||||
"description": "required fields",
|
||||
"type": "string"
|
||||
},
|
||||
"firstname": {
|
||||
"description": "optional fields",
|
||||
"type": "string"
|
||||
},
|
||||
"isAdmin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastname": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"description": "optional, integrated password authentication",
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"ApiBasicAuth": {
|
||||
"type": "basic"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
type swaggerInfo struct {
|
||||
Version string
|
||||
Host string
|
||||
BasePath string
|
||||
Schemes []string
|
||||
Title string
|
||||
Description string
|
||||
}
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = swaggerInfo{
|
||||
Version: "1.0",
|
||||
Host: "",
|
||||
BasePath: "/api/v1",
|
||||
Schemes: []string{},
|
||||
Title: "WireGuard Portal API",
|
||||
Description: "WireGuard Portal API for managing users and peers.",
|
||||
}
|
||||
|
||||
type s struct{}
|
||||
|
||||
func (s *s) ReadDoc() string {
|
||||
sInfo := SwaggerInfo
|
||||
sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
|
||||
|
||||
t, err := template.New("swagger_info").Funcs(template.FuncMap{
|
||||
"marshal": func(v interface{}) string {
|
||||
a, _ := json.Marshal(v)
|
||||
return string(a)
|
||||
},
|
||||
}).Parse(doc)
|
||||
if err != nil {
|
||||
return doc
|
||||
}
|
||||
|
||||
var tpl bytes.Buffer
|
||||
if err := t.Execute(&tpl, sInfo); err != nil {
|
||||
return doc
|
||||
}
|
||||
|
||||
return tpl.String()
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(swag.Name, &s{})
|
||||
}
|
@ -2,9 +2,14 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
wgportal "github.com/h44z/wg-portal"
|
||||
"github.com/h44z/wg-portal/internal/authentication"
|
||||
_ "github.com/h44z/wg-portal/internal/server/docs" // docs is generated by Swag CLI, you have to import it.
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/swaggo/gin-swagger/swaggerFiles"
|
||||
)
|
||||
|
||||
func SetupRoutes(s *Server) {
|
||||
@ -60,6 +65,23 @@ func SetupRoutes(s *Server) {
|
||||
user.GET("/status", s.GetPeerStatus)
|
||||
}
|
||||
|
||||
func SetupApiRoutes(s *Server) {
|
||||
api := ApiServer{s: s}
|
||||
|
||||
// Auth routes
|
||||
apiV1 := s.server.Group("/api/v1")
|
||||
apiV1.Use(s.RequireApiAuthentication("admin"))
|
||||
apiV1.GET("/users", api.GetUsers)
|
||||
apiV1.POST("/users", api.PostUser)
|
||||
apiV1.GET("/user/:email", api.GetUser)
|
||||
apiV1.PUT("/user/:email", api.PutUser)
|
||||
apiV1.PATCH("/user/:email", api.PatchUser)
|
||||
apiV1.DELETE("/user/:email", api.DeleteUser)
|
||||
|
||||
// Swagger doc/ui
|
||||
s.server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
|
||||
func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSessionData(c)
|
||||
@ -78,7 +100,7 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// default case if some randome scope was set...
|
||||
// default case if some random scope was set...
|
||||
if scope != "" && !session.IsAdmin {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
@ -90,3 +112,67 @@ func (s *Server) RequireAuthentication(scope string) gin.HandlerFunc {
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) RequireApiAuthentication(scope string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
username, password, hasAuth := c.Request.BasicAuth()
|
||||
if !hasAuth {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate form input
|
||||
if strings.Trim(username, " ") == "" || strings.Trim(password, " ") == "" {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check user database for an matching entry
|
||||
var loginProvider authentication.AuthProvider
|
||||
user := s.users.GetUser(username) // retrieve active candidate user from db
|
||||
if user == nil || user.Email == "" {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
loginProvider = s.auth.GetProvider(string(user.Source))
|
||||
if loginProvider == nil {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
authEmail, err := loginProvider.Login(&authentication.AuthContext{
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
|
||||
// Test if authentication succeeded
|
||||
if err != nil || authEmail == "" {
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check admin scope
|
||||
if scope == "admin" && !user.IsAdmin {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// default case if some random scope was set...
|
||||
if scope != "" && !user.IsAdmin {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusForbidden, ApiError{Message: "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Continue down the chain to handler etc
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
@ -151,6 +151,7 @@ func (s *Server) Setup(ctx context.Context) error {
|
||||
|
||||
// Setup all routes
|
||||
SetupRoutes(s)
|
||||
SetupApiRoutes(s)
|
||||
|
||||
// Setup user database (also needed for database authentication)
|
||||
s.users, err = users.NewManager(s.db)
|
||||
|
Loading…
Reference in New Issue
Block a user