updates
Server Deploy / deploy (push) Successful in 2m16s

This commit is contained in:
domrichardson
2026-06-15 16:20:26 +01:00
parent e215ccc979
commit aaf154168e
14 changed files with 482 additions and 30 deletions
+19
View File
@@ -11,6 +11,17 @@ services:
retries: 5 retries: 5
start_period: 20s start_period: 20s
redis:
image: redis:8
restart: unless-stopped
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
server: server:
build: build:
context: ../server context: ../server
@@ -21,14 +32,21 @@ services:
- "9090:9090" - "9090:9090"
environment: environment:
MONGO_URI: mongodb://mongo:27017/keymanager MONGO_URI: mongodb://mongo:27017/keymanager
REDIS_ADDR: redis:6379
GITEA_HOST: ${GITEA_HOST} GITEA_HOST: ${GITEA_HOST}
PUBLIC_HOST: ${PUBLIC_HOST} PUBLIC_HOST: ${PUBLIC_HOST}
GRPC_HOST: ${GRPC_HOST} GRPC_HOST: ${GRPC_HOST}
GRPC_PORT: "9090" GRPC_PORT: "9090"
HTTP_PORT: "8080" HTTP_PORT: "8080"
OIDC_ISSUER: ${OIDC_ISSUER:-}
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
OIDC_REDIRECT_URL: ${OIDC_REDIRECT_URL:-}
depends_on: depends_on:
mongo: mongo:
condition: service_healthy condition: service_healthy
redis:
condition: service_healthy
web: web:
build: build:
@@ -44,3 +62,4 @@ services:
volumes: volumes:
mongo_data: mongo_data:
redis_data:
+12
View File
@@ -1,12 +1,14 @@
package main package main
import ( import (
"context"
"log" "log"
"os" "os"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/mrhid6/keymanager/server/internal/api" "github.com/mrhid6/keymanager/server/internal/api"
"github.com/mrhid6/keymanager/server/internal/auth"
"github.com/mrhid6/keymanager/server/internal/db" "github.com/mrhid6/keymanager/server/internal/db"
grpcserver "github.com/mrhid6/keymanager/server/internal/grpc" grpcserver "github.com/mrhid6/keymanager/server/internal/grpc"
"github.com/mrhid6/keymanager/server/internal/services" "github.com/mrhid6/keymanager/server/internal/services"
@@ -21,6 +23,16 @@ func main() {
} }
log.Println("connected to MongoDB") log.Println("connected to MongoDB")
redisAddr := getEnv("REDIS_ADDR", "localhost:6379")
if err := auth.InitRedis(redisAddr); err != nil {
log.Fatalf("failed to connect to Redis: %v", err)
}
log.Println("connected to Redis")
if err := auth.InitOIDC(context.Background()); err != nil {
log.Fatalf("failed to initialise OIDC: %v", err)
}
// Background goroutine to mark offline servers // Background goroutine to mark offline servers
go func() { go func() {
ticker := time.NewTicker(2 * time.Minute) ticker := time.NewTicker(2 * time.Minute)
+7 -1
View File
@@ -3,19 +3,24 @@ module github.com/mrhid6/keymanager/server
go 1.26 go 1.26
require ( require (
github.com/coreos/go-oidc/v3 v3.18.0
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/redis/go-redis/v9 v9.20.1
go.mongodb.org/mongo-driver/v2 v2.2.2 go.mongodb.org/mongo-driver/v2 v2.2.2
golang.org/x/oauth2 v0.36.0
google.golang.org/grpc v1.64.0 google.golang.org/grpc v1.64.0
) )
require ( require (
github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect
@@ -23,7 +28,7 @@ require (
github.com/golang/snappy v1.0.0 // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -35,6 +40,7 @@ require (
github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.33.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.25.0 // indirect
+20 -3
View File
@@ -1,11 +1,19 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -15,6 +23,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -37,8 +47,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
@@ -53,6 +63,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w=
github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -78,8 +90,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 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.mongodb.org/mongo-driver/v2 v2.2.2 h1:9cYuS3fl1Xhqwpfazso10V7BHQD58kCgtzhfAmJYz9c= go.mongodb.org/mongo-driver/v2 v2.2.2 h1:9cYuS3fl1Xhqwpfazso10V7BHQD58kCgtzhfAmJYz9c=
go.mongodb.org/mongo-driver/v2 v2.2.2/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps= go.mongodb.org/mongo-driver/v2 v2.2.2/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
@@ -93,6 +109,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
@@ -102,7 +120,6 @@ 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-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-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+41 -17
View File
@@ -6,6 +6,7 @@ import (
"os" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/mrhid6/keymanager/server/internal/auth"
"github.com/mrhid6/keymanager/server/internal/models" "github.com/mrhid6/keymanager/server/internal/models"
"github.com/mrhid6/keymanager/server/internal/services" "github.com/mrhid6/keymanager/server/internal/services"
) )
@@ -13,21 +14,30 @@ import (
func RegisterRoutes(r *gin.Engine) { func RegisterRoutes(r *gin.Engine) {
r.GET("/install", handleInstallScript) r.GET("/install", handleInstallScript)
api := r.Group("/api") // Auth endpoints (no session required)
{ r.GET("/auth/login", auth.HandleLogin)
api.GET("/servers", listServers) r.GET("/auth/callback", auth.HandleCallback)
api.POST("/servers", createServer) r.GET("/auth/logout", auth.HandleLogout)
api.GET("/servers/new", newServer) r.GET("/auth/me", auth.HandleMe)
api.POST("/servers/new", newServer)
api.GET("/servers/:id", getServer)
api.DELETE("/servers/:id", deleteServer)
api.POST("/servers/:id/generate-key", generateKey)
api.GET("/keys", listKeys) // API endpoints protected by session middleware
api.POST("/keys", createKey) apiGroup := r.Group("/api")
api.GET("/keys/:id", getKey) apiGroup.Use(auth.Middleware())
api.POST("/keys/:id/assign", assignKey) {
api.DELETE("/keys/:id/assign/:serverId", revokeAssignment) apiGroup.GET("/servers", listServers)
apiGroup.POST("/servers", createServer)
apiGroup.GET("/servers/new", newServer)
apiGroup.POST("/servers/new", newServer)
apiGroup.GET("/servers/:id", getServer)
apiGroup.DELETE("/servers/:id", deleteServer)
apiGroup.POST("/servers/:id/generate-key", generateKey)
apiGroup.GET("/keys", listKeys)
apiGroup.POST("/keys", createKey)
apiGroup.GET("/keys/:id", getKey)
apiGroup.DELETE("/keys/:id", deleteKey)
apiGroup.POST("/keys/:id/assign", assignKey)
apiGroup.DELETE("/keys/:id/assign/:serverId", revokeAssignment)
} }
} }
@@ -163,12 +173,26 @@ func getKey(c *gin.Context) {
} }
assignments, _ := services.GetAssignmentsWithServers(id) assignments, _ := services.GetAssignmentsWithServers(id)
c.JSON(http.StatusOK, gin.H{
"key": key, type keyResponse struct {
"assignments": assignments, *models.Key
Assignments any `json:"assignments"`
}
c.JSON(http.StatusOK, keyResponse{
Key: key,
Assignments: assignments,
}) })
} }
func deleteKey(c *gin.Context) {
id := c.Param("id")
if err := services.DeleteKey(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"deleted": true})
}
func assignKey(c *gin.Context) { func assignKey(c *gin.Context) {
keyID := c.Param("id") keyID := c.Param("id")
var body struct { var body struct {
+39
View File
@@ -0,0 +1,39 @@
package auth
import (
"net/http"
"github.com/gin-gonic/gin"
)
const ctxSessionKey = "km_session"
func GetSessionFromContext(c *gin.Context) *Session {
v, _ := c.Get(ctxSessionKey)
sess, _ := v.(*Session)
return sess
}
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !authEnabled {
c.Next()
return
}
cookie, err := c.Request.Cookie(sessionCookieName)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
return
}
sess, err := GetSession(c.Request.Context(), cookie.Value)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "session expired"})
return
}
c.Set(ctxSessionKey, sess)
c.Next()
}
}
+154
View File
@@ -0,0 +1,154 @@
package auth
import (
"context"
"log"
"net/http"
"os"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
)
var (
oidcProvider *oidc.Provider
oauth2Cfg *oauth2.Config
authEnabled bool
)
func InitOIDC(ctx context.Context) error {
issuer := os.Getenv("OIDC_ISSUER")
if issuer == "" {
log.Println("OIDC_ISSUER not set; authentication disabled")
return nil
}
p, err := oidc.NewProvider(ctx, issuer)
if err != nil {
return err
}
oidcProvider = p
oauth2Cfg = &oauth2.Config{
ClientID: os.Getenv("OIDC_CLIENT_ID"),
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
RedirectURL: os.Getenv("OIDC_REDIRECT_URL"),
Endpoint: p.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
authEnabled = true
log.Println("OIDC authentication enabled")
return nil
}
func Enabled() bool { return authEnabled }
func HandleLogin(c *gin.Context) {
state, err := randomHex(16)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "state generation failed"})
return
}
if err := SaveState(c.Request.Context(), state); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "state save failed"})
return
}
c.Redirect(http.StatusFound, oauth2Cfg.AuthCodeURL(state))
}
func HandleCallback(c *gin.Context) {
ctx := c.Request.Context()
if !ConsumeState(ctx, c.Query("state")) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid state"})
return
}
token, err := oauth2Cfg.Exchange(ctx, c.Query("code"))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "token exchange failed"})
return
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "missing id_token"})
return
}
verifier := oidcProvider.Verifier(&oidc.Config{ClientID: oauth2Cfg.ClientID})
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "token verification failed"})
return
}
var claims struct {
Sub string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
}
if err := idToken.Claims(&claims); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "claims extraction failed"})
return
}
sessionID, err := SaveSession(ctx, &Session{
UserID: claims.Sub,
Email: claims.Email,
Name: claims.Name,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"})
return
}
secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https"
http.SetCookie(c.Writer, &http.Cookie{
Name: sessionCookieName,
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
MaxAge: int(sessionTTL.Seconds()),
})
frontendURL := os.Getenv("PUBLIC_HOST")
if frontendURL == "" {
frontendURL = "/"
}
c.Redirect(http.StatusFound, frontendURL)
}
func HandleLogout(c *gin.Context) {
if cookie, err := c.Request.Cookie(sessionCookieName); err == nil {
_ = DeleteSession(c.Request.Context(), cookie.Value)
}
http.SetCookie(c.Writer, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
HttpOnly: true,
MaxAge: -1,
})
c.Redirect(http.StatusFound, "/")
}
func HandleMe(c *gin.Context) {
if !authEnabled {
c.JSON(http.StatusOK, gin.H{"auth_enabled": false})
return
}
cookie, err := c.Request.Cookie(sessionCookieName)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
return
}
sess, err := GetSession(c.Request.Context(), cookie.Value)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "session expired"})
return
}
c.JSON(http.StatusOK, sess)
}
+79
View File
@@ -0,0 +1,79 @@
package auth
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
)
const sessionTTL = 24 * time.Hour
const sessionCookieName = "km_session"
const sessionPrefix = "km:session:"
const statePrefix = "km:state:"
type Session struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
}
var rdb *redis.Client
func InitRedis(addr string) error {
rdb = redis.NewClient(&redis.Options{Addr: addr})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return rdb.Ping(ctx).Err()
}
func randomHex(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
func SaveSession(ctx context.Context, sess *Session) (string, error) {
id, err := randomHex(32)
if err != nil {
return "", err
}
data, err := json.Marshal(sess)
if err != nil {
return "", err
}
if err := rdb.Set(ctx, sessionPrefix+id, data, sessionTTL).Err(); err != nil {
return "", err
}
return id, nil
}
func GetSession(ctx context.Context, id string) (*Session, error) {
data, err := rdb.Get(ctx, sessionPrefix+id).Bytes()
if err != nil {
return nil, err
}
var sess Session
if err := json.Unmarshal(data, &sess); err != nil {
return nil, err
}
return &sess, nil
}
func DeleteSession(ctx context.Context, id string) error {
return rdb.Del(ctx, sessionPrefix+id).Err()
}
func SaveState(ctx context.Context, state string) error {
return rdb.Set(ctx, statePrefix+state, "1", 10*time.Minute).Err()
}
func ConsumeState(ctx context.Context, state string) bool {
n, err := rdb.Del(ctx, statePrefix+state).Result()
return err == nil && n > 0
}
+16 -2
View File
@@ -63,7 +63,12 @@ func GetKey(keyID string) (*models.Key, error) {
return &key, nil return &key, nil
} }
func ListKeys() ([]models.Key, error) { type KeyWithCount struct {
models.Key `bson:",inline"`
AssignedCount int `bson:"-" json:"assigned_count"`
}
func ListKeys() ([]KeyWithCount, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@@ -77,7 +82,16 @@ func ListKeys() ([]models.Key, error) {
if err := cursor.All(ctx, &keys); err != nil { if err := cursor.All(ctx, &keys); err != nil {
return nil, err return nil, err
} }
return keys, nil
result := make([]KeyWithCount, 0, len(keys))
for _, k := range keys {
count, _ := db.Col("assignments").CountDocuments(ctx, bson.M{
"key_id": k.KeyID,
"revoked_at": nil,
})
result = append(result, KeyWithCount{Key: k, AssignedCount: int(count)})
}
return result, nil
} }
func DeleteKey(keyID string) error { func DeleteKey(keyID string) error {
+9 -6
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import { Providers } from "@/components/Providers"; import { Providers } from "@/components/Providers";
import { AuthProvider } from "@/components/AuthProvider";
import { Sidebar } from "@/components/Sidebar"; import { Sidebar } from "@/components/Sidebar";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -17,12 +18,14 @@ export default function RootLayout({
<html lang="en" className="dark"> <html lang="en" className="dark">
<body className="bg-background text-text-primary"> <body className="bg-background text-text-primary">
<Providers> <Providers>
<div className="flex h-screen overflow-hidden"> <AuthProvider>
<Sidebar /> <div className="flex h-screen overflow-hidden">
<main className="flex-1 overflow-y-auto"> <Sidebar />
{children} <main className="flex-1 overflow-y-auto">
</main> {children}
</div> </main>
</div>
</AuthProvider>
</Providers> </Providers>
</body> </body>
</html> </html>
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
export interface User {
user_id: string;
email: string;
name: string;
}
interface AuthContextType {
user: User | null;
authEnabled: boolean;
}
const AuthContext = createContext<AuthContextType>({ user: null, authEnabled: false });
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [authEnabled, setAuthEnabled] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/auth/me", { credentials: "include" })
.then(async (res) => {
if (res.status === 401) {
window.location.href = "/auth/login";
return;
}
const data = await res.json();
if (data.auth_enabled === false) {
setAuthEnabled(false);
} else {
setAuthEnabled(true);
setUser(data as User);
}
setLoading(false);
})
.catch(() => {
// Backend unreachable — don't block the UI
setLoading(false);
});
}, []);
if (loading) {
return (
<div className="flex h-screen items-center justify-center bg-background">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-accent" />
</div>
);
}
return (
<AuthContext.Provider value={{ user, authEnabled }}>
{children}
</AuthContext.Provider>
);
}
+19 -1
View File
@@ -3,6 +3,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useAuth } from "@/components/AuthProvider";
interface NavItem { interface NavItem {
href: string; href: string;
@@ -33,6 +34,7 @@ const navItems: NavItem[] = [
export function Sidebar() { export function Sidebar() {
const pathname = usePathname(); const pathname = usePathname();
const { user, authEnabled } = useAuth();
return ( return (
<aside className="flex h-screen w-60 flex-col border-r border-border bg-surface"> <aside className="flex h-screen w-60 flex-col border-r border-border bg-surface">
@@ -71,7 +73,23 @@ export function Sidebar() {
</nav> </nav>
<div className="border-t border-border px-4 py-3"> <div className="border-t border-border px-4 py-3">
<p className="text-xs text-text-secondary">KeyManager v1.0</p> {authEnabled && user && (
<div className="mb-3">
<p className="truncate text-sm font-medium text-text-primary">{user.name || user.email}</p>
<p className="truncate text-xs text-text-secondary">{user.email}</p>
</div>
)}
<div className="flex items-center justify-between">
<p className="text-xs text-text-secondary">KeyManager v1.0</p>
{authEnabled && user && (
<a
href="/auth/logout"
className="text-xs text-text-secondary transition-colors hover:text-danger"
>
Logout
</a>
)}
</div>
</div> </div>
</aside> </aside>
); );
+1
View File
@@ -58,6 +58,7 @@ class ApiError extends Error {
async function request<T>(path: string, options?: RequestInit): Promise<T> { async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`/api${path}`, { const res = await fetch(`/api${path}`, {
credentials: "include",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...options?.headers, ...options?.headers,
+4
View File
@@ -10,6 +10,10 @@ const nextConfig: NextConfig = {
source: "/api/:path*", source: "/api/:path*",
destination: `${apiUrl}/api/:path*`, destination: `${apiUrl}/api/:path*`,
}, },
{
source: "/auth/:path*",
destination: `${apiUrl}/auth/:path*`,
},
{ {
source: "/install", source: "/install",
destination: `${apiUrl}/install`, destination: `${apiUrl}/install`,