diff --git a/Dockerfile b/Dockerfile index 32f5078..40edbe9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ ARG PORT ENV PORT=$PORT # Copy the built app from the builder stage +COPY ./.temp/wireguard-tools /bin/wireguard-api COPY ./.temp/wireguard-api /bin/wireguard-api COPY ./.temp/api-key-generator /bin/api-key-generator diff --git a/DockerfileBuild b/DockerfileBuild index e65043c..7ecebbb 100644 --- a/DockerfileBuild +++ b/DockerfileBuild @@ -1,5 +1,5 @@ # Use the official Golang image -FROM golang:1.22.5-alpine as builder +FROM golang:1.23-alpine as builder # Set the working directory WORKDIR /app @@ -24,8 +24,6 @@ FROM alpine:latest # Install necessary dependencies RUN apk --no-cache add ca-certificates -RUN mkdir -p /output - # Copy the built app from the builder stage COPY --from=builder /app/wireguard-api /bin/wireguard-api COPY --from=builder /app/api-key-generator /bin/api-key-generator diff --git a/Makefile b/Makefile index e7f6d46..a4e3d73 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ ENV = ./dev/.env # Start all services in detached mode up: - @$(DOCKER_COMPOSE) --env-file $(ENV) --project-name private-network -f $(DOCKER_COMPOSE_FILE) up -d + @$(DOCKER_COMPOSE) --env-file $(ENV) --project-name private-network -f $(DOCKER_COMPOSE_FILE) up --build -d @echo "Docker services are now running." + @make logs # Stop all running services down: @@ -28,7 +29,7 @@ rebuild: # Show logs from Docker Compose services logs: - @$(DOCKER_COMPOSE) --project-name wireguard-api -f $(DOCKER_COMPOSE_FILE) logs -f + @docker logs -f wireguard-api # Clean up dangling images, stopped containers, and unused networks clean: diff --git a/README.md b/README.md index f27a874..9ec49c9 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The application reads its configuration from the following environment variables | `CONTEXT_PATH` | The base path used for API routing. | `/api/private-network/v1/` | No | | `PEERS_RESOURCE_PATH` | Filesystem path for peer resource connections. | `/etc/wireguard/` | No | | `API_INIT_FILE` | Add the first user data that will be created at the application startup | N/A | No | +| `DEBUG_MODE` | Set `log.SetFlags(log.LstdFlags \| log.Llongfile)` | N/A | No | ### Database Configuration (`DBEnv`) diff --git a/cmd/cli/connect/get_peer.go b/cmd/cli/connect/get_peer.go index a5e149a..78453c1 100644 --- a/cmd/cli/connect/get_peer.go +++ b/cmd/cli/connect/get_peer.go @@ -1,8 +1,8 @@ package connect import ( + "github.com/softwareplace/http-utils/api_context" "github.com/softwareplace/http-utils/request" - httputilsserver "github.com/softwareplace/http-utils/server" "github.com/softwareplace/wireguard-api/cmd/cli/spec" "github.com/softwareplace/wireguard-api/pkg/models" "log" @@ -14,7 +14,7 @@ func GetPeer(profile *spec.Profile, server *spec.Server) models.Peer { apiConfig := request.Build(server.Host). WithPath("/peers"). - WithHeader(httputilsserver.XApiKey, server.ApiKey). + WithHeader(api_context.XApiKey, server.ApiKey). WithHeader("Authorization", profile.AuthorizationKey). WithExpectedStatusCode(http.StatusOK) diff --git a/cmd/cli/connect/login.go b/cmd/cli/connect/login.go index 8024656..c925971 100644 --- a/cmd/cli/connect/login.go +++ b/cmd/cli/connect/login.go @@ -2,8 +2,8 @@ package connect import ( "fmt" + "github.com/softwareplace/http-utils/api_context" "github.com/softwareplace/http-utils/request" - httputilsserver "github.com/softwareplace/http-utils/server" "github.com/softwareplace/wireguard-api/cmd/cli/shared" "github.com/softwareplace/wireguard-api/cmd/cli/spec" "github.com/softwareplace/wireguard-api/pkg/utils/sec" @@ -76,7 +76,7 @@ func Login(args *shared.Args, profile *spec.Profile, server spec.Server) { config := request.Build(server.Host). WithPath("/login"). WithBody(reqBody). - WithHeader(httputilsserver.XApiKey, server.ApiKey). + WithHeader(api_context.XApiKey, server.ApiKey). WithExpectedStatusCode(http.StatusOK) loginResp, err := api.Post(config) diff --git a/cmd/generator/api_secret/main.go b/cmd/generator/api_secret/main.go index 52c183a..0e28b3f 100644 --- a/cmd/generator/api_secret/main.go +++ b/cmd/generator/api_secret/main.go @@ -7,9 +7,11 @@ import ( "encoding/pem" "flag" "github.com/atotto/clipboard" + "github.com/softwareplace/http-utils/security" "github.com/softwareplace/wireguard-api/pkg/domain/db" "github.com/softwareplace/wireguard-api/pkg/domain/repository/api_secret" - "github.com/softwareplace/wireguard-api/pkg/domain/service/security" + "github.com/softwareplace/wireguard-api/pkg/domain/service/userPrincipalService" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" "github.com/softwareplace/wireguard-api/pkg/models" "github.com/softwareplace/wireguard-api/pkg/utils/env" "log" @@ -97,7 +99,9 @@ func main() { Bytes: publicKeyBytes, }) - encryptedKey, err := security.GetApiSecurityService().Encrypt(string(publicKeyPEM)) + principalService := userPrincipalService.GetUserPrincipalService() + securityService := security.ApiSecurityServiceBuild[*request.ApiContext](appEnv.ApiSecretAuthorization, principalService) + encryptedKey, err := securityService.Encrypt(string(publicKeyPEM)) if err != nil { log.Fatalf("Failed to sec public key: %s", err) @@ -119,13 +123,14 @@ func main() { return } + expirationToken := time.Hour * (time.Duration(*expirationHours)) apiJWTInfo := security.ApiJWTInfo{ Client: *clientInfo, - Expiration: time.Duration(*expirationHours), + Expiration: expirationToken, Key: *id, } - apiSecretJWT, err := security.GetApiSecurityService().GenerateApiSecretJWT(apiJWTInfo) + apiSecretJWT, err := securityService.GenerateApiSecretJWT(apiJWTInfo) if err != nil { return diff --git a/cmd/server/main.go b/cmd/server/main.go index ead7ed2..ba2841b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,24 +1,53 @@ package main import ( + "github.com/softwareplace/http-utils/security" "github.com/softwareplace/http-utils/server" - "github.com/softwareplace/wireguard-api/pkg/auth" "github.com/softwareplace/wireguard-api/pkg/domain/db" + "github.com/softwareplace/wireguard-api/pkg/domain/service/apiSecretService" "github.com/softwareplace/wireguard-api/pkg/domain/service/peer" - "github.com/softwareplace/wireguard-api/pkg/domain/service/user" + "github.com/softwareplace/wireguard-api/pkg/domain/service/userPrincipalService" + "github.com/softwareplace/wireguard-api/pkg/domain/service/user_service" "github.com/softwareplace/wireguard-api/pkg/handlers" "github.com/softwareplace/wireguard-api/pkg/handlers/request" + "github.com/softwareplace/wireguard-api/pkg/utils/env" ) +var ( + userService user_service.Service + securityService security.ApiSecurityService[*request.ApiContext] + secreteAccessHandler security.ApiSecretAccessHandler[*request.ApiContext] + userLoginService server.LoginService[*request.ApiContext] +) + +func factory(appEnv env.ApplicationEnv) { + userService = user_service.GetService() + secretKeyProvider := apiSecretService.GetSecretKeyProvider() + principalService := userPrincipalService.GetUserPrincipalService() + securityService = security.ApiSecurityServiceBuild(appEnv.ApiSecretAuthorization, principalService) + + secreteAccessHandler = security.ApiSecretAccessHandlerBuild( + appEnv.ApiSecretKey, + secretKeyProvider, + securityService, + ) + userLoginService = user_service.GetLoginService(securityService) +} + func main() { + appEnv := env.AppEnv() db.InitMongoDB() - api := server.New() - api.Router().Use(request.ContextBuilder) - handler := auth.NewApiSecurityHandler() - api.Router().Use(handler.Middleware) - api.Router().Use(auth.AccessValidation) + + factory(appEnv) + + api := server.CreateApiRouter[*request.ApiContext](). + RegisterMiddleware(secreteAccessHandler.HandlerSecretAccess, security.ApiSecretAccessHandlerName). + RegisterMiddleware(securityService.AuthorizationHandler, security.ApiSecurityHandlerName). + WithLoginResource(userLoginService) + handlers.Init(api) + userService.Init() peer.GetService().Load() - user.GetService().Init() + api.StartServer() } diff --git a/cmd/stream/main.go b/cmd/stream/main.go index 867ad62..88e628b 100644 --- a/cmd/stream/main.go +++ b/cmd/stream/main.go @@ -2,10 +2,10 @@ package main import ( "fmt" + "github.com/softwareplace/http-utils/api_context" + "github.com/softwareplace/http-utils/request" "github.com/softwareplace/wireguard-api/cmd/stream/parse" "github.com/softwareplace/wireguard-api/cmd/stream/spec" - "github.com/softwareplace/wireguard-api/pkg/handlers/request" - "github.com/softwareplace/wireguard-api/pkg/http_api" "go/types" "log" "net/http" @@ -47,13 +47,13 @@ func main() { //fmt.Println("Dump as JSON:") //fmt.Println(string(jsonDump)) - api := http_api.NewApi(types.Nil{}) - config := http_api.Config(streamEnv.Server). + api := request.NewApi(types.Nil{}) + config := request.Build(streamEnv.Server). WithPath("peers/stream"). WithHeader("Authorization", streamEnv.Authorization). - WithHeader(request.XApiKey, streamEnv.ApiKey). + WithHeader(api_context.XApiKey, streamEnv.ApiKey). WithBody(dump). - WithExpectedStatusCode(http.StatusCreated) + WithExpectedStatusCode(http.StatusOK) _, err = api.Post(config) if err != nil { diff --git a/dev/.env b/dev/.env index f8b8e52..20a4b6f 100644 --- a/dev/.env +++ b/dev/.env @@ -6,9 +6,10 @@ MONGO_PORT=27017 MONGO_CONTAINER_NAME=wireguard-api-mongo # WireGuard API Configuration -PORT=8080 +PORT=1080 PEERS_RESOURCE_PATH=./dev/peers -API_SECRET_PATH=./dev/v1/secret +CONTEXT_PATH=/api/private-network/v1/ +API_SECRET_PATH=./dev/secret/v1/ API_SECRET_KEY=/etc/secret/private.key APP_CONTAINER_NAME=wireguard-api MONGO_URI=mongodb://${MONGO_CONTAINER_NAME}:${MONGO_PORT} diff --git a/dev/test.sh b/dev/test.sh new file mode 100644 index 0000000..e387be0 --- /dev/null +++ b/dev/test.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +mkdir .out + +# Number of iterations +COUNT=1000000 + +# The curl command +URL='http://localhost:1080/api/private-network/v1/login' +API_KEY='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlLZXkiOiJFQnFtMGgxS2o0M2RxSC8rSG05L2hEZjFBelpxdFhOeUpZRVhwMDZ0YzRjanZ0RFU2cU40eXc9PSIsImNsaWVudCI6IlNvZnR3YXJlIFBsYWNlIENPIiwiZXhwIjoyMDUyNzM3ODM1fQ.PaC_hYLbGMjv9ANJO1Ch09ul0nrMUkGnXM28Z1iLLr0' +USERNAME='my-username' +PASSWORD='ynT9558iiMga&ayTVGs3Gc6ug1' + + +response=$(curl --silent --location --write-out "%{http_code}" --output ./.out/response.json "$URL" \ + --header "X-Api-Key: $API_KEY" \ + --header "Content-Type: application/json" \ + --data "{ + \"username\": \"$USERNAME\", + \"password\": \"$PASSWORD\" + }") + + +makePeersRequest() { + local AUTHORIZATION + AUTHORIZATION=$1 + local PEERS_URL + PEERS_URL='http://localhost:1080/api/private-network/v1/peers' + + for i in $(seq 1 $COUNT); do + echo "Request #$i" + curl --silent --output ./.out/response.json --location "$PEERS_URL?key=$i" \ + --header "X-Api-Key: $API_KEY" \ + --header "Authorization: $AUTHORIZATION" \ + --header "Content-Type: application/json" + + cat ./.out/response.json || jq || true + done +} + +if [ "$response" -eq 200 ]; then + token=$(jq -r '.token' ./.out/response.json) + makePeersRequest "$token" +else + echo "Request failed with status code: $response" +fi +# +#for i in $(seq 1 $COUNT); do +# echo "Request #$i" +# +#done + +echo "Done sending $COUNT requests." diff --git a/docker-compose.yml b/docker-compose.yml index 9b6cd62..95a0298 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,7 @@ services: - ${PEERS_RESOURCE_PATH}:/etc/wireguard - ${API_SECRET_PATH}:/etc/secret environment: + - CONTEXT_PATH=${CONTEXT_PATH} - API_SECRET_KEY=${API_SECRET_KEY} - MONGO_URI=${MONGO_URI} - MONGO_USERNAME=${MONGO_INITDB_ROOT_USERNAME} diff --git a/go.mod b/go.mod index 6ab922f..203590f 100644 --- a/go.mod +++ b/go.mod @@ -6,25 +6,25 @@ toolchain go1.23.4 require ( github.com/atotto/clipboard v0.1.4 - github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/google/uuid v1.6.0 - github.com/gorilla/mux v1.8.1 - go.mongodb.org/mongo-driver v1.17.1 - golang.org/x/crypto v0.31.0 + github.com/softwareplace/http-utils v0.0.0-20250119222044-a46592dfd464 + go.mongodb.org/mongo-driver v1.17.2 + golang.org/x/crypto v0.32.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/montanaflynn/stats v0.7.1 // indirect - github.com/softwareplace/http-utils v0.0.0-20250115004038-6ed463f52d1a // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index 5bc9289..c134bbd 100644 --- a/go.sum +++ b/go.sum @@ -16,14 +16,16 @@ github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IX github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/softwareplace/http-utils v0.0.0-20250115001316-f7f5d1f4f930 h1:umXPNgmmtm7nxaguVOHvWMutLs72fc3dDIPgd17xThg= -github.com/softwareplace/http-utils v0.0.0-20250115001316-f7f5d1f4f930/go.mod h1:qbgKuvJXcx4I6HEOmi1yFSfX8MpygEAj7s5ggceNSUk= -github.com/softwareplace/http-utils v0.0.0-20250115002306-419dc927f49f h1:f1FN/OEJ0tp2CcBKxWOkt6nlNfw4danBXThv2P2Yido= -github.com/softwareplace/http-utils v0.0.0-20250115002306-419dc927f49f/go.mod h1:qbgKuvJXcx4I6HEOmi1yFSfX8MpygEAj7s5ggceNSUk= -github.com/softwareplace/http-utils v0.0.0-20250115003340-50c4c89e24bd h1:Nl6ggz2YYIceKD1wi6MzdQUtVxQFGdRFIQQCI5MbYgA= -github.com/softwareplace/http-utils v0.0.0-20250115003340-50c4c89e24bd/go.mod h1:qbgKuvJXcx4I6HEOmi1yFSfX8MpygEAj7s5ggceNSUk= -github.com/softwareplace/http-utils v0.0.0-20250115004038-6ed463f52d1a h1:UIF8CFvYjmBBzHcIOdNXfimJVdvP1uuuY1zRRbwCh9k= -github.com/softwareplace/http-utils v0.0.0-20250115004038-6ed463f52d1a/go.mod h1:qbgKuvJXcx4I6HEOmi1yFSfX8MpygEAj7s5ggceNSUk= +github.com/softwareplace/http-utils v0.0.0-20250118214521-e0c79d734a9b h1:v4r1YBVnRtvn/umb2jsZ+ETwo5vd7a07b/rbxi2qrDQ= +github.com/softwareplace/http-utils v0.0.0-20250118214521-e0c79d734a9b/go.mod h1:rMDg/RflN3SzoboxvRA1EqcJE897mduCdKvfHCNKsCg= +github.com/softwareplace/http-utils v0.0.0-20250119193252-ad2358849935 h1:K4MwOi5kfI1OK5nfYvR+AYTOmi/6WX4a6xgP7Zj53U8= +github.com/softwareplace/http-utils v0.0.0-20250119193252-ad2358849935/go.mod h1:rMDg/RflN3SzoboxvRA1EqcJE897mduCdKvfHCNKsCg= +github.com/softwareplace/http-utils v0.0.0-20250119205956-21b32f554f09 h1:WnVR8eszlyqlCIM1xLgafB/UoDn5BKsPtQS5XI9aEZY= +github.com/softwareplace/http-utils v0.0.0-20250119205956-21b32f554f09/go.mod h1:rMDg/RflN3SzoboxvRA1EqcJE897mduCdKvfHCNKsCg= +github.com/softwareplace/http-utils v0.0.0-20250119211455-787e57a219f1 h1:aUhJM9lgw3vNxoIxo7M+D0Z0ZcheRKdy6iWYBjkGvEE= +github.com/softwareplace/http-utils v0.0.0-20250119211455-787e57a219f1/go.mod h1:rMDg/RflN3SzoboxvRA1EqcJE897mduCdKvfHCNKsCg= +github.com/softwareplace/http-utils v0.0.0-20250119222044-a46592dfd464 h1:d/Zjb1DwmIxej6sanKe0sg6WY+hzk9ZQO/73tOP8TbU= +github.com/softwareplace/http-utils v0.0.0-20250119222044-a46592dfd464/go.mod h1:rMDg/RflN3SzoboxvRA1EqcJE897mduCdKvfHCNKsCg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -33,12 +35,12 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= -go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= +go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= +go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -52,12 +54,12 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/pkg/auth/access_secret.go b/pkg/auth/access_secret.go deleted file mode 100644 index d6a7d85..0000000 --- a/pkg/auth/access_secret.go +++ /dev/null @@ -1,155 +0,0 @@ -package auth - -import ( - "crypto/ecdsa" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "github.com/softwareplace/http-utils/server" - "github.com/softwareplace/wireguard-api/pkg/domain/repository/api_secret" - "github.com/softwareplace/wireguard-api/pkg/handlers/request" - "github.com/softwareplace/wireguard-api/pkg/utils/env" - "log" - "net/http" - "os" -) - -var ( - // apiSecret is expected to be an environment variable, adjust as needed - apiSecret any // Replace with logic to fetch from environment variables or similar - appEnv = env.AppEnv() -) - -type ApiSecurityHandler interface { - InitAPISecretKey() - Middleware(next http.Handler) http.Handler - ValidatePublicKey(ctx server.ApiRequestContext) error -} - -type apiSecurityHandlerImpl struct{} - -func NewApiSecurityHandler() ApiSecurityHandler { - return &apiSecurityHandlerImpl{} -} - -func (a *apiSecurityHandlerImpl) Middleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Validate the public key - ctx := server.Of(w, r, "MIDDLEWARE/API_SECRET") - - if err := a.ValidatePublicKey(ctx); err != nil { - ctx.Error("You are not allowed to access this resource", http.StatusUnauthorized) - return - } - - ctx.Next(next) - }) -} - -// InitAPISecretKey initializes and validates the apiSecret environment variable. -// If apiSecret is provided, it ensures the private key from the specified path can be loaded. -// The application will crash if the private key cannot be loaded. -func (a *apiSecurityHandlerImpl) InitAPISecretKey() { - secretKey := appEnv.ApiSecretKey - // Load private key from the provided secretKey file path - privateKeyData, err := os.ReadFile(secretKey) - if err != nil { - log.Fatalf("Failed to read private key file: %s", err.Error()) - } - - // Decode PEM block from the private key data - block, _ := pem.Decode(privateKeyData) - if block == nil || block.Type != "PRIVATE KEY" { - log.Fatalf("Failed to decode private key PEM block") - } - - // Parse the private key using ParsePKCS8PrivateKey - privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - log.Fatalf("Failed to parse private key: %s", err.Error()) - } - apiSecret = privateKey - - switch key := apiSecret.(type) { - case *ecdsa.PrivateKey: - log.Println("Loaded ECDSA private key successfully") - case *rsa.PrivateKey: - log.Println("Loaded RSA private key successfully") - default: - log.Fatalf("Unsupported private key type: %T", key) - } -} - -// ValidatePublicKey validates a given public key (in Base64 format) against the private key (apiSecret). -// This is performed only if mustValidatePublicKey is true. -func (a *apiSecurityHandlerImpl) ValidatePublicKey(ctx server.ApiRequestContext) error { - // Decode the Base64-encoded public key - claims, err := apiSecurityService.JWTClaims(ctx) - - if err != nil { - return err - } - apiContext := ctx.RequestData.(request.ApiContext) - - apiContext.SetApiKeyClaims(claims) - - id, err := apiSecurityService.Decrypt(claims["key"].(string)) - - if err != nil { - return err - } - - apiAccessSecret, err := api_secret.GetRepository().GetById(id) - apiContext.SetApiKeyId(id) - if err != nil { - return err - } - - // Decode the PEM-encoded public key - decryptKey, err := apiSecurityService.Decrypt(apiAccessSecret.Key) - if err != nil { - return err - } - block, _ := pem.Decode([]byte(decryptKey)) - if block == nil || block.Type != "PUBLIC KEY" { - return fmt.Errorf("failed to decode PEM public key") - } - - // Parse the public key - parsedPublicKey, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return fmt.Errorf("failed to parse public key: %w", err) - } - - switch privateKey := apiSecret.(type) { - case *ecdsa.PrivateKey: - // Ensure the type of the public key matches ECDSA - publicKey, ok := parsedPublicKey.(*ecdsa.PublicKey) - if !ok { - return fmt.Errorf("invalid public key type, expected ECDSA") - } - - // Validate if the public key corresponds to the private key - privateKeyPubKey := &privateKey.PublicKey - if publicKey.X.Cmp(privateKeyPubKey.X) != 0 || publicKey.Y.Cmp(privateKeyPubKey.Y) != 0 { - return fmt.Errorf("public key does not match the private key") - } - case *rsa.PrivateKey: - // Ensure the type of the public key matches RSA - publicKey, ok := parsedPublicKey.(*rsa.PublicKey) - if !ok { - return fmt.Errorf("invalid public key type, expected RSA") - } - - // Validate if the public key corresponds to the private key - privateKeyPubKey := &privateKey.PublicKey - if publicKey.E != privateKeyPubKey.E || publicKey.N.Cmp(privateKeyPubKey.N) != 0 { - return fmt.Errorf("public key does not match the private key") - } - default: - return fmt.Errorf("unsupported private key type: %T", privateKey) - } - - return nil -} diff --git a/pkg/auth/access_validation.go b/pkg/auth/access_validation.go deleted file mode 100644 index 397c7ef..0000000 --- a/pkg/auth/access_validation.go +++ /dev/null @@ -1,88 +0,0 @@ -package auth - -import ( - "github.com/softwareplace/http-utils/server" - "github.com/softwareplace/wireguard-api/pkg/handlers/request" - "github.com/softwareplace/wireguard-api/pkg/models" - "log" - "net/http" -) - -func AccessValidation(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - openPathLock.RLock() - defer openPathLock.RUnlock() - matchFound := false - for _, path := range openPath { - if path == r.Method+"::"+r.URL.Path { - matchFound = true - break - } - } - - ctx := server.Of(w, r, "MIDDLEWARE/ACCESS_VALIDATION") - - if !matchFound { - _, success := apiSecurityService.Validation(ctx, _nextValidation) - if !success { - return - } - - if !hasResourceAccess(ctx) { - return - } - } - - ctx.Next(next) - }) -} - -func hasResourceAccess(ctx server.ApiRequestContext) bool { - apiContext := ctx.RequestData.(request.ApiContext) - - userRoles, err := apiContext.GetRoles() - - if err != nil { - ctx.Error("You are not allowed to access this resource", http.StatusUnauthorized) - return false - } - - accessRoles, hasRoles := GetRolesForPath(ctx.Request) - - if !hasRoles { - ctx.Error("You are not allowed to access this resource", http.StatusUnauthorized) - return false - } - - hasAccess := false - - for _, userRole := range userRoles { - for _, accessRole := range accessRoles { - if userRole == accessRole { - hasAccess = true - break - } - } - if hasAccess { - break - } - } - - if !hasAccess { - ctx.Error("You are not allowed to access this resource", http.StatusUnauthorized) - return false - } - return true -} - -func _nextValidation(ctx server.ApiRequestContext) (*models.User, bool) { - apiContext := ctx.RequestData.(request.ApiContext) - - userData, err := usersRepo.FindUserBySalt(apiContext.AccessId) - if err != nil { - log.Printf("Failed to valiaded user %v access: %v", ctx, err) - return nil, false - } - apiContext.SetUser(userData) - return userData, true -} diff --git a/pkg/auth/data.go b/pkg/auth/data.go deleted file mode 100644 index ac5cb49..0000000 --- a/pkg/auth/data.go +++ /dev/null @@ -1,36 +0,0 @@ -package auth - -import ( - "github.com/softwareplace/wireguard-api/pkg/domain/repository/user" - "github.com/softwareplace/wireguard-api/pkg/domain/service/security" - "net/http" - "sync" -) - -var ( - roles = make(map[string][]string) - openPath []string - openPathLock sync.RWMutex - apiSecurityService = security.GetApiSecurityService() - usersRepo = user.Repository() -) - -func AddOpenPath(path string) { - openPathLock.Lock() - defer openPathLock.Unlock() - openPath = append(openPath, path) -} - -func AddRoles(path string, requiredRoles ...string) { - if len(requiredRoles) > 0 { - roles[path] = requiredRoles - } -} - -func GetRolesForPath(r *http.Request) ([]string, bool) { - path := r.Method + "::" + r.URL.Path - if roles[path] != nil { - return roles[path], true - } - return nil, false -} diff --git a/pkg/domain/db/db.go b/pkg/domain/db/db.go index 501509f..2293bea 100644 --- a/pkg/domain/db/db.go +++ b/pkg/domain/db/db.go @@ -6,19 +6,29 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "log" + "sync" ) -var dbEnv = env.AppEnv().DBEnv +var ( + dbEnv env.DBEnv + mongoDbOnce sync.Once + mongoDbInstance *mongo.Database +) + +func GetDB() *mongo.Database { + dbEnv = env.AppEnv().DBEnv + mongoDbOnce.Do(func() { + mongoDbInstance = GetDBClient().Database(dbEnv.DatabaseName) + }) + return mongoDbInstance +} func InitMongoDB() { + dbEnv = env.AppEnv().DBEnv connectionChecker() GetDB() } -func GetDB() *mongo.Database { - return GetDBClient().Database(dbEnv.DatabaseName) -} - func GetDBClient() *mongo.Client { username := dbEnv.Username diff --git a/pkg/domain/repository/user/users_repository.go b/pkg/domain/repository/user/users_repository.go index 7086a59..6a82bc3 100644 --- a/pkg/domain/repository/user/users_repository.go +++ b/pkg/domain/repository/user/users_repository.go @@ -4,6 +4,7 @@ import ( "github.com/softwareplace/wireguard-api/pkg/domain/db" "github.com/softwareplace/wireguard-api/pkg/models" "go.mongodb.org/mongo-driver/mongo" + "sync" ) type UsersRepository interface { @@ -23,8 +24,16 @@ func (r *usersRepositoryImpl) collection() *mongo.Collection { return r.database.Collection("users") } +var ( + repositoryInstance UsersRepository + repositoryOnce sync.Once +) + func Repository() UsersRepository { - return &usersRepositoryImpl{ - database: db.GetDB(), - } + repositoryOnce.Do(func() { + repositoryInstance = &usersRepositoryImpl{ + database: db.GetDB(), + } + }) + return repositoryInstance } diff --git a/pkg/domain/service/apiSecretService/service.go b/pkg/domain/service/apiSecretService/service.go new file mode 100644 index 0000000..be95493 --- /dev/null +++ b/pkg/domain/service/apiSecretService/service.go @@ -0,0 +1,36 @@ +package apiSecretService + +import ( + "github.com/softwareplace/http-utils/api_context" + "github.com/softwareplace/http-utils/security" + "github.com/softwareplace/wireguard-api/pkg/domain/repository/api_secret" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" + "sync" +) + +type apiSecretKeyProviderImpl struct { + repository api_secret.ApiSecretRepository +} + +var ( + apiSecretKeyProviderOnce sync.Once + apiSecretKeyProviderInstance security.ApiSecretKeyProvider[*request.ApiContext] +) + +func GetSecretKeyProvider() security.ApiSecretKeyProvider[*request.ApiContext] { + apiSecretKeyProviderOnce.Do(func() { + apiSecretKeyProviderInstance = &apiSecretKeyProviderImpl{ + repository: api_secret.GetRepository(), + } + }) + + return apiSecretKeyProviderInstance +} + +func (s *apiSecretKeyProviderImpl) Get(ctx *api_context.ApiRequestContext[*request.ApiContext]) (string, error) { + apiSecret, err := s.repository.GetById(ctx.ApiKeyId) + if err != nil { + return "", err + } + return apiSecret.Key, nil +} diff --git a/pkg/domain/service/peer/service.go b/pkg/domain/service/peer/service.go index 5d8c725..296de3d 100644 --- a/pkg/domain/service/peer/service.go +++ b/pkg/domain/service/peer/service.go @@ -3,6 +3,7 @@ package peer import ( "github.com/softwareplace/wireguard-api/pkg/domain/repository/peer" "github.com/softwareplace/wireguard-api/pkg/models" + "sync" ) type Service interface { @@ -19,6 +20,14 @@ func (s *serviceImpl) repository() peer.Repository { return peer.GetRepository() } +var ( + serviceInstance Service + serviceOnce sync.Once +) + func GetService() Service { - return &serviceImpl{} + serviceOnce.Do(func() { + serviceInstance = &serviceImpl{} + }) + return serviceInstance } diff --git a/pkg/domain/service/security/api_secret_jwt.go b/pkg/domain/service/security/api_secret_jwt.go deleted file mode 100644 index ae1ede7..0000000 --- a/pkg/domain/service/security/api_secret_jwt.go +++ /dev/null @@ -1,26 +0,0 @@ -package security - -import ( - "github.com/dgrijalva/jwt-go" - "time" -) - -// GenerateApiSecretJWT creates a JWT token with the username and role -func (a *apiSecurityServiceImpl) GenerateApiSecretJWT(jwtInfo ApiJWTInfo) (string, error) { - secret := a.Secret() - - encryptedKey, err := a.Encrypt(jwtInfo.Key) - if err != nil { - return "", err - } - - duration := time.Hour * jwtInfo.Expiration - expiration := time.Now().Add(duration).Unix() - claims := jwt.MapClaims{ - "client": jwtInfo.Client, - "key": encryptedKey, - "exp": expiration, - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(secret) -} diff --git a/pkg/domain/service/security/def.go b/pkg/domain/service/security/def.go deleted file mode 100644 index c341d06..0000000 --- a/pkg/domain/service/security/def.go +++ /dev/null @@ -1,39 +0,0 @@ -package security - -import ( - "github.com/softwareplace/http-utils/server" - "github.com/softwareplace/wireguard-api/pkg/models" - "time" -) - -type ApiSecurityService interface { - Secret() []byte - GenerateApiSecretJWT(jwtInfo ApiJWTInfo) (string, error) - ExtractJWTClaims(requestContext server.ApiRequestContext) bool - JWTClaims(ctx server.ApiRequestContext) (map[string]interface{}, error) - GenerateJWT(user models.User) (map[string]interface{}, error) - Encrypt(key string) (string, error) - Decrypt(encrypted string) (string, error) - Validation( - ctx server.ApiRequestContext, - next func(ctx server.ApiRequestContext) (*models.User, bool), - ) (*models.User, bool) -} - -type ApiJWTInfo struct { - Client string - Key string - // Expiration in hours - Expiration time.Duration // -} - -type apiSecurityServiceImpl struct{} - -var ( - instance = &apiSecurityServiceImpl{} -) - -func GetApiSecurityService() ApiSecurityService { - instance.Secret() - return instance -} diff --git a/pkg/domain/service/security/encryptor.go b/pkg/domain/service/security/encryptor.go deleted file mode 100644 index b5f062e..0000000 --- a/pkg/domain/service/security/encryptor.go +++ /dev/null @@ -1,13 +0,0 @@ -package security - -import "github.com/softwareplace/wireguard-api/pkg/utils/sec" - -func (a *apiSecurityServiceImpl) Encrypt(value string) (string, error) { - secret := a.Secret() - return sec.Encrypt(value, secret) -} - -func (a *apiSecurityServiceImpl) Decrypt(encrypted string) (string, error) { - secret := a.Secret() - return sec.Decrypt(encrypted, secret) -} diff --git a/pkg/domain/service/security/jwt.go b/pkg/domain/service/security/jwt.go deleted file mode 100644 index af82c3d..0000000 --- a/pkg/domain/service/security/jwt.go +++ /dev/null @@ -1,116 +0,0 @@ -package security - -import ( - "fmt" - "github.com/dgrijalva/jwt-go" - "github.com/softwareplace/http-utils/server" - "github.com/softwareplace/wireguard-api/pkg/handlers/request" - "github.com/softwareplace/wireguard-api/pkg/models" - envUtils "github.com/softwareplace/wireguard-api/pkg/utils/env" - "log" - "net/http" - "strconv" - "time" -) - -func (a *apiSecurityServiceImpl) Validation( - ctx server.ApiRequestContext, - next func(requestContext server.ApiRequestContext, - ) (*models.User, bool)) (*models.User, bool) { - success := a.ExtractJWTClaims(ctx) - - if !success { - return nil, success - } - - user, success := next(ctx) - - if !success { - ctx.Error("Authorization failed", http.StatusForbidden) - return nil, success - } - accessUserContext := ctx.RequestData.(request.ApiContext) - - accessUserContext.SetUser(user) - return user, success -} - -func (a *apiSecurityServiceImpl) ExtractJWTClaims(ctx server.ApiRequestContext) bool { - apiContext := ctx.RequestData.(request.ApiContext) - - token, err := jwt.Parse(ctx.Authorization, func(token *jwt.Token) (interface{}, error) { - return a.Secret(), nil - }) - - if err != nil { - log.Printf("JWT/PARSE: Authorization failed: %v", err) - ctx.Error("Authorization failed", http.StatusForbidden) - return false - } - - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - apiContext.SetAuthorizationClaims(claims) - - requester, err := a.Decrypt(claims["request"].(string)) - - if err != nil { - log.Printf("JWT/CLAIMS_EXTRACT: Authorization failed: %v", err) - ctx.Error("Authorization failed", http.StatusForbidden) - return false - } - - apiContext.SetAccessId(requester) - - return true - } - - log.Printf("JWT/CLAIMS_EXTRACT: failed with error_handler: %v", err) - ctx.Error("Authorization failed", http.StatusForbidden) - return false -} - -func (a *apiSecurityServiceImpl) JWTClaims(ctx server.ApiRequestContext) (map[string]interface{}, error) { - token, err := jwt.Parse(ctx.ApiKey, func(token *jwt.Token) (interface{}, error) { - return a.Secret(), nil - }) - - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - return claims, nil - } - - return nil, fmt.Errorf("failed to extract jwt claims") -} - -func (a *apiSecurityServiceImpl) Secret() []byte { - secret := envUtils.AppEnv().ApiSecretAuthorization - return []byte(secret) -} - -// GenerateJWT creates a JWT token with the username and role -func (a *apiSecurityServiceImpl) GenerateJWT(user models.User) (map[string]interface{}, error) { - duration := time.Minute * 15 - expiration := time.Now().Add(duration).Unix() - requestBy, err := a.Encrypt(user.Salt) - - var encryptedRoles []string - for _, role := range user.Roles { - encryptedRole, err := a.Encrypt(role) - if err != nil { - return nil, err - } - encryptedRoles = append(encryptedRoles, encryptedRole) - } - - claims := jwt.MapClaims{ - "request": requestBy, - "scope": encryptedRoles, - "exp": expiration, - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signedToken, err := token.SignedString(a.Secret()) - return map[string]interface{}{"token": signedToken, "expires": strconv.FormatInt(expiration, 10)}, err -} diff --git a/pkg/domain/service/userPrincipalService/user_principal_service.go b/pkg/domain/service/userPrincipalService/user_principal_service.go new file mode 100644 index 0000000..47ea253 --- /dev/null +++ b/pkg/domain/service/userPrincipalService/user_principal_service.go @@ -0,0 +1,38 @@ +package userPrincipalService + +import ( + "github.com/softwareplace/http-utils/api_context" + "github.com/softwareplace/http-utils/security/principal" + "github.com/softwareplace/wireguard-api/pkg/domain/repository/user" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" + "sync" +) + +type UserPrincipalService struct { + userRepository user.UsersRepository +} + +var ( + userPrincipalServiceOnce sync.Once + userPrincipalServiceInstance principal.PService[*request.ApiContext] +) + +func GetUserPrincipalService() principal.PService[*request.ApiContext] { + userPrincipalServiceOnce.Do(func() { + userPrincipalServiceInstance = &UserPrincipalService{ + userRepository: user.Repository(), + } + }) + return userPrincipalServiceInstance +} + +func (u *UserPrincipalService) LoadPrincipal(ctx *api_context.ApiRequestContext[*request.ApiContext]) bool { + userResponse, err := u.userRepository.FindUserBySalt(ctx.AccessId) + if err != nil { + return false + } + + context := request.NewApiContext(userResponse.Parse()) + ctx.Principal = &context + return true +} diff --git a/pkg/domain/service/user_service/login_service.go b/pkg/domain/service/user_service/login_service.go new file mode 100644 index 0000000..6aefb8d --- /dev/null +++ b/pkg/domain/service/user_service/login_service.go @@ -0,0 +1,49 @@ +package user_service + +import ( + "github.com/softwareplace/http-utils/security" + "github.com/softwareplace/http-utils/server" + "github.com/softwareplace/wireguard-api/pkg/domain/repository/user" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" + "sync" + "time" +) + +type loginServiceImpl struct { + securityService security.ApiSecurityService[*request.ApiContext] + repository user.UsersRepository +} + +func (l *loginServiceImpl) SecurityService() security.ApiSecurityService[*request.ApiContext] { + return l.securityService +} + +var ( + loginServiceOnce sync.Once + loginServiceInstance server.LoginService[*request.ApiContext] +) + +func GetLoginService(securityService security.ApiSecurityService[*request.ApiContext]) server.LoginService[*request.ApiContext] { + loginServiceOnce.Do(func() { + loginServiceInstance = &loginServiceImpl{ + securityService: securityService, + repository: user.Repository(), + } + }) + + return loginServiceInstance +} + +func (l *loginServiceImpl) Login(user server.LoginEntryData) (*request.ApiContext, error) { + response, err := l.repository.FindUserByUsernameOrEmail(user.Username, user.Email) + if err != nil { + return nil, err + } + return &request.ApiContext{ + User: response.Parse(), + }, nil +} + +func (l *loginServiceImpl) TokenDuration() time.Duration { + return time.Minute * 15 +} diff --git a/pkg/domain/service/user/service.go b/pkg/domain/service/user_service/service.go similarity index 85% rename from pkg/domain/service/user/service.go rename to pkg/domain/service/user_service/service.go index 07491b4..4de19b3 100644 --- a/pkg/domain/service/user/service.go +++ b/pkg/domain/service/user_service/service.go @@ -1,4 +1,4 @@ -package user +package user_service import ( repo "github.com/softwareplace/wireguard-api/pkg/domain/repository/user" @@ -8,6 +8,7 @@ import ( "github.com/softwareplace/wireguard-api/pkg/utils/sec" "github.com/softwareplace/wireguard-api/pkg/utils/validator" "log" + "sync" ) type Service interface { @@ -19,11 +20,19 @@ type serviceImpl struct { repository repo.UsersRepository } +var ( + serviceOnce sync.Once + serviceInstance Service +) + func GetService() Service { - return &serviceImpl{ - appEnv: env.AppEnv(), - repository: repo.Repository(), - } + serviceOnce.Do(func() { + serviceInstance = &serviceImpl{ + appEnv: env.AppEnv(), + repository: repo.Repository(), + } + }) + return serviceInstance } type userInit struct { diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index 057c763..63dac09 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -3,10 +3,11 @@ package handlers import ( "github.com/softwareplace/http-utils/server" "github.com/softwareplace/wireguard-api/pkg/handlers/peer" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" "github.com/softwareplace/wireguard-api/pkg/handlers/user" ) -func Init(api server.ApiRouterHandler) { +func Init(api server.ApiRouterHandler[*request.ApiContext]) { user.Init(api) peer.Init(api) } diff --git a/pkg/handlers/peer/get_available.go b/pkg/handlers/peer/get_available.go index cae501b..6d5d3bb 100644 --- a/pkg/handlers/peer/get_available.go +++ b/pkg/handlers/peer/get_available.go @@ -1,12 +1,13 @@ package peer import ( - "github.com/softwareplace/http-utils/server" + "github.com/softwareplace/http-utils/api_context" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" "log" "net/http" ) -func (h *handlerImpl) GetAvailablePeer(ctx *server.ApiRequestContext) { +func (h *handlerImpl) GetAvailablePeer(ctx *api_context.ApiRequestContext[*request.ApiContext]) { peer, err, notFound := h.Service().GetAvailablePeer() if notFound { log.Printf("[%s]:: no peer available: %v", ctx.GetSessionId(), err) diff --git a/pkg/handlers/peer/handlers.go b/pkg/handlers/peer/handlers.go index d42c933..fe2cdb1 100644 --- a/pkg/handlers/peer/handlers.go +++ b/pkg/handlers/peer/handlers.go @@ -1,13 +1,15 @@ package peer import ( + "github.com/softwareplace/http-utils/api_context" "github.com/softwareplace/http-utils/server" "github.com/softwareplace/wireguard-api/pkg/domain/service/peer" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" ) type Handler interface { - GetAvailablePeer(ctx *server.ApiRequestContext) - Stream(ctx *server.ApiRequestContext) + GetAvailablePeer(ctx *api_context.ApiRequestContext[*request.ApiContext]) + Stream(ctx *api_context.ApiRequestContext[*request.ApiContext]) Service() peer.Service } @@ -21,7 +23,7 @@ func (h *handlerImpl) Service() peer.Service { return peer.GetService() } -func Init(api server.ApiRouterHandler) { +func Init(api server.ApiRouterHandler[*request.ApiContext]) { handler := GetHandler() api.Get(handler.GetAvailablePeer, "peers", "resource:peers:get:peer") api.Post(handler.Stream, "peers/stream", "resource:peers:stream:peers") diff --git a/pkg/handlers/peer/stream.go b/pkg/handlers/peer/stream.go index 24c5166..58c6b8d 100644 --- a/pkg/handlers/peer/stream.go +++ b/pkg/handlers/peer/stream.go @@ -1,17 +1,19 @@ package peer import ( + "github.com/softwareplace/http-utils/api_context" "github.com/softwareplace/http-utils/server" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" "github.com/softwareplace/wireguard-api/pkg/models" "log" "net/http" ) -func (h *handlerImpl) Stream(ctx *server.ApiRequestContext) { +func (h *handlerImpl) Stream(ctx *api_context.ApiRequestContext[*request.ApiContext]) { server.GetRequestBody(ctx, []models.Peer{}, h.save, server.FailedToLoadBody) } -func (h *handlerImpl) save(ctx *server.ApiRequestContext, peers []models.Peer) { +func (h *handlerImpl) save(ctx *api_context.ApiRequestContext[*request.ApiContext], peers []models.Peer) { err := h.Service().Stream(peers) if err != nil { log.Printf("[%s]:: error saving peers: %v", ctx.GetSessionId(), err) diff --git a/pkg/handlers/request/context.go b/pkg/handlers/request/context.go index 8e5e9e9..cdd8ede 100644 --- a/pkg/handlers/request/context.go +++ b/pkg/handlers/request/context.go @@ -1,70 +1,31 @@ package request -import ( - "fmt" - "github.com/softwareplace/http-utils/server" - "github.com/softwareplace/wireguard-api/pkg/models" - "net/http" -) +type UserPrincipal struct { + Username string + Email string + Salt string + Roles []string + Status string +} type ApiContext struct { - User *models.User + User *UserPrincipal AccessId string ApiKeyId string AuthorizationClaims map[string]interface{} ApiKeyClaims map[string]interface{} } -func ContextBuilder(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := server.Of(w, r, "MIDDLEWARE/CONTEXT_BUILDER") - ctx.RequestData = &ApiContext{} - ctx.Next(next) - }) -} - -func (ctx *ApiContext) GetAccessApiKeyId() string { - if ctx.ApiKeyId != "" { - return ctx.ApiKeyId +func NewApiContext(user *UserPrincipal) *ApiContext { + return &ApiContext{ + User: user, } - return "N/A" -} - -func (ctx *ApiContext) GetAccessId() string { - if ctx.AccessId != "" { - return ctx.AccessId - } - return "N/A" -} - -func (ctx *ApiContext) new() *ApiContext { - return &ApiContext{} -} - -func (ctx *ApiContext) GetRoles() (roles []string, err error) { - user := ctx.User - if user != nil && len(user.Roles) > 0 { - return user.Roles, nil - } - return nil, fmt.Errorf("user roles not found") -} - -func (ctx *ApiContext) SetUser(user *models.User) { - ctx.User = user -} - -func (ctx *ApiContext) SetAuthorizationClaims(authorizationClaims map[string]interface{}) { - ctx.AuthorizationClaims = authorizationClaims -} - -func (ctx *ApiContext) SetApiKeyClaims(apiKeyClaims map[string]interface{}) { - ctx.ApiKeyClaims = apiKeyClaims } -func (ctx *ApiContext) SetApiKeyId(apiKeyId string) { - ctx.ApiKeyId = apiKeyId +func (ctx *ApiContext) GetSalt() string { + return ctx.User.Salt } -func (ctx *ApiContext) SetAccessId(accessId string) { - ctx.AccessId = accessId +func (ctx *ApiContext) GetRoles() []string { + return ctx.User.Roles } diff --git a/pkg/handlers/user/create.go b/pkg/handlers/user/create.go index 91c0ddd..486c3e7 100644 --- a/pkg/handlers/user/create.go +++ b/pkg/handlers/user/create.go @@ -1,7 +1,9 @@ package user import ( + "github.com/softwareplace/http-utils/api_context" "github.com/softwareplace/http-utils/server" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" "github.com/softwareplace/wireguard-api/pkg/models" "github.com/softwareplace/wireguard-api/pkg/utils/sec" "github.com/softwareplace/wireguard-api/pkg/utils/validator" @@ -9,11 +11,11 @@ import ( "net/http" ) -func (h *handlerImpl) CreateUser(ctx *server.ApiRequestContext) { +func (h *handlerImpl) CreateUser(ctx *api_context.ApiRequestContext[*request.ApiContext]) { server.GetRequestBody(ctx, models.User{}, h.validateUserFields, server.FailedToLoadBody) } -func (h *handlerImpl) validateUserFields(ctx *server.ApiRequestContext, user models.User) { +func (h *handlerImpl) validateUserFields(ctx *api_context.ApiRequestContext[*request.ApiContext], user models.User) { if err := validator.ValidateUserFields(user); err != nil { log.Printf("[%s]:: validation failed with error: %v", ctx.GetSessionId(), err) ctx.Error(err.Error(), http.StatusBadRequest) diff --git a/pkg/handlers/user/handlers.go b/pkg/handlers/user/handlers.go index f2f48e1..8547fc0 100644 --- a/pkg/handlers/user/handlers.go +++ b/pkg/handlers/user/handlers.go @@ -1,34 +1,31 @@ package user import ( + "github.com/softwareplace/http-utils/api_context" + "github.com/softwareplace/http-utils/security" "github.com/softwareplace/http-utils/server" "github.com/softwareplace/wireguard-api/pkg/domain/repository/user" - "github.com/softwareplace/wireguard-api/pkg/domain/service/security" - "net/http" + "github.com/softwareplace/wireguard-api/pkg/handlers/request" ) type Handler interface { UsersRepository() user.UsersRepository - Login(w http.ResponseWriter, r *http.Request) - CreateUser(w http.ResponseWriter, r *http.Request) - UpdateUser(w http.ResponseWriter, r *http.Request) + Login(ctx *api_context.ApiRequestContext[*request.ApiContext]) + CreateUser(ctx *api_context.ApiRequestContext[*request.ApiContext]) + UpdateUser(ctx *api_context.ApiRequestContext[*request.ApiContext]) + JWTService() security.ApiSecurityService[*request.ApiContext] Init() - JWTService() security.ApiSecurityService } -type handlerImpl struct{} +type handlerImpl struct { +} func (h *handlerImpl) UsersRepository() user.UsersRepository { return user.Repository() } -func (h *handlerImpl) ApiSecurityService() security.ApiSecurityService { - return security.GetApiSecurityService() -} - -func Init(api server.ApiRouterHandler) { +func Init(api server.ApiRouterHandler[*request.ApiContext]) { handler := handlerImpl{} - api.PublicRouter(handler.Login, "login", "POST") api.Post(handler.CreateUser, "user", "POST", "resource:users:create:user") api.Put(handler.UpdateUser, "user/:id", "resource:users:update:user") api.Put(handler.UpdateUser, "user") diff --git a/pkg/handlers/user/login.go b/pkg/handlers/user/login.go deleted file mode 100644 index fc40672..0000000 --- a/pkg/handlers/user/login.go +++ /dev/null @@ -1,54 +0,0 @@ -package user - -import ( - "errors" - "github.com/softwareplace/http-utils/server" - - "github.com/softwareplace/wireguard-api/pkg/models" - "github.com/softwareplace/wireguard-api/pkg/utils/sec" - "github.com/softwareplace/wireguard-api/pkg/utils/validator" - "go.mongodb.org/mongo-driver/mongo" - "log" -) - -func (h *handlerImpl) Login(ctx *server.ApiRequestContext) { - server.GetRequestBody(ctx, models.User{}, h.checkUserCredentials, server.FailedToLoadBody) -} - -func (h *handlerImpl) checkUserCredentials(ctx *server.ApiRequestContext, userInput models.User) { - decrypt, err := sec.Decrypt(userInput.Password, []byte(sec.SampleEncryptKey)) - - if err != nil { - log.Printf("Failed to decrypt password: %v", err) - } else { - userInput.Password = decrypt - } - - // Validate userResponse credentials - userResponse, err := h.UsersRepository().FindUserByUsernameOrEmail(userInput.Username, userInput.Email) - if err != nil { - if errors.Is(err, mongo.ErrNoDocuments) { - ctx.Unauthorized() - return - } - ctx.InternalServerError("Internal Server Error") - log.Printf("[%s]:: find user by username or email failed: %v", ctx.GetSessionId(), err) - - return - } - - if !validator.CheckPassword(userInput.Password, userResponse.Password, userResponse.Salt) { - ctx.Unauthorized() - return - } - - // Generate JWT and respond - tokenData, err := h.ApiSecurityService().GenerateJWT(*userResponse) - if err != nil { - log.Printf("[%s]:: generating new jwt failed: %v", ctx.GetSessionId(), err) - ctx.InternalServerError("Error generating token") - return - } - - ctx.Ok(tokenData) -} diff --git a/pkg/handlers/user/update.go b/pkg/handlers/user/update.go index 9af7fe6..f993d04 100644 --- a/pkg/handlers/user/update.go +++ b/pkg/handlers/user/update.go @@ -1,6 +1,7 @@ package user import ( + "github.com/softwareplace/http-utils/api_context" "github.com/softwareplace/http-utils/server" "github.com/softwareplace/wireguard-api/pkg/handlers/request" "github.com/softwareplace/wireguard-api/pkg/models" @@ -9,13 +10,12 @@ import ( "net/http" ) -func (h *handlerImpl) UpdateUser(ctx *server.ApiRequestContext) { +func (h *handlerImpl) UpdateUser(ctx *api_context.ApiRequestContext[*request.ApiContext]) { server.GetRequestBody(ctx, models.UserUpdate{}, h.useUpdateValidation, server.FailedToLoadBody) } -func (h *handlerImpl) useUpdateValidation(ctx *server.ApiRequestContext, updatedUser models.UserUpdate) { - apiContext := ctx.RequestData.(request.ApiContext) - currentUser, err := h.UsersRepository().FindUserBySalt(apiContext.AccessId) +func (h *handlerImpl) useUpdateValidation(ctx *api_context.ApiRequestContext[*request.ApiContext], updatedUser models.UserUpdate) { + currentUser, err := h.UsersRepository().FindUserBySalt(ctx.AccessId) if err != nil { log.Printf("[%s]:: find user by salt failed: %v", ctx.GetSessionId(), err) diff --git a/pkg/models/user.go b/pkg/models/user.go index 9bcbba0..611309a 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -1,6 +1,7 @@ package models import ( + "github.com/softwareplace/wireguard-api/pkg/handlers/request" "go.mongodb.org/mongo-driver/bson/primitive" ) @@ -22,3 +23,13 @@ type UserUpdate struct { Password string `json:"password"` Email string `json:"email"` } + +func (user *User) Parse() *request.UserPrincipal { + return &request.UserPrincipal{ + Username: user.Username, + Email: user.Email, + Salt: user.Salt, + Roles: user.Roles, + Status: user.Status, + } +} diff --git a/pkg/utils/env/envs.go b/pkg/utils/env/envs.go index b785c84..d04ffb2 100644 --- a/pkg/utils/env/envs.go +++ b/pkg/utils/env/envs.go @@ -1,7 +1,9 @@ package env import ( + "log" "os" + "sync" ) type ApplicationEnv struct { @@ -31,28 +33,39 @@ type DBEnv struct { Uri string } -var appEnv *ApplicationEnv +var ( + instance *ApplicationEnv + appEnvOnce sync.Once +) func AppEnv() ApplicationEnv { - if appEnv == nil { - dbEnv := DBEnv{ - DatabaseName: GetRequiredEnv("MONGO_DATABASE"), // required - Username: GetRequiredEnv("MONGO_USERNAME"), // required - Password: GetRequiredEnv("MONGO_PASSWORD"), // required - Uri: GetRequiredEnv("MONGO_URI"), // required - } + if os.Getenv("DEBUG_MODE") == "true" { + log.SetFlags(log.LstdFlags | log.Llongfile) + } + + appEnvOnce.Do(func() { + if instance == nil { - appEnv = &ApplicationEnv{ - ApiSecretAuthorization: GetRequiredEnv("API_SECRET_AUTHORIZATION"), // required - Port: getServerPort(), - ContextPath: getServerContextPath(), - PeerResourcePath: getPeerResourcePath(), - ApiSecretKey: GetRequiredEnv("API_SECRET_KEY"), - InitFilePath: os.Getenv("API_INIT_FILE"), - DBEnv: dbEnv, + dbEnv := DBEnv{ + DatabaseName: GetRequiredEnv("MONGO_DATABASE"), // required + Username: GetRequiredEnv("MONGO_USERNAME"), // required + Password: GetRequiredEnv("MONGO_PASSWORD"), // required + Uri: GetRequiredEnv("MONGO_URI"), // required + } + + instance = &ApplicationEnv{ + ApiSecretAuthorization: GetRequiredEnv("API_SECRET_AUTHORIZATION"), // required + Port: getServerPort(), + ContextPath: getServerContextPath(), + PeerResourcePath: getPeerResourcePath(), + ApiSecretKey: GetRequiredEnv("API_SECRET_KEY"), + InitFilePath: os.Getenv("API_INIT_FILE"), + DBEnv: dbEnv, + } } - } - return *appEnv + }) + + return *instance } func getPeerResourcePath() string { diff --git a/scripts/docker/compiler/build b/scripts/docker/compiler/build index 18ef1c6..75006bf 100644 --- a/scripts/docker/compiler/build +++ b/scripts/docker/compiler/build @@ -3,7 +3,7 @@ mkdir -p /output cp /bin/wireguard-api /output/wireguard-api -cp /bin/api-key-generator /output/api-key-generator +cp /bin/api-key-generator /output/api-key-generator cp /bin/wireguard-tools /output/wireguard-tools cp /bin/wireguard-stream /output/wireguard-stream