First commit
commit
db87e3be3d
|
|
@ -0,0 +1,21 @@
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/main .
|
||||||
|
COPY --from=builder /app/internal/web ./internal/web
|
||||||
|
COPY --from=builder /app/certs ./certs
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["./main"]
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"pki-manager/config"
|
||||||
|
"pki-manager/internal/api"
|
||||||
|
"pki-manager/internal/repository"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Configuration
|
||||||
|
cfg := config.LoadConfig()
|
||||||
|
|
||||||
|
// MongoDB connection
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
clientOptions := options.Client().ApplyURI(cfg.MongoDBURI)
|
||||||
|
client, err := mongo.Connect(ctx, clientOptions)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer client.Disconnect(ctx)
|
||||||
|
|
||||||
|
// Verify connection
|
||||||
|
err = client.Ping(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
log.Println("Connected to MongoDB!")
|
||||||
|
|
||||||
|
// Initialize repository
|
||||||
|
repo := repository.NewMongoRepository(client.Database(cfg.DBName))
|
||||||
|
|
||||||
|
// Create certs directory if not exists
|
||||||
|
if err := os.MkdirAll("certs", 0755); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup Gin
|
||||||
|
if cfg.Environment == "production" {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := gin.Default()
|
||||||
|
|
||||||
|
// Setup routes
|
||||||
|
api.SetupRoutes(router, repo, cfg.JWTSecret)
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
log.Printf("Server starting on port %s", cfg.Port)
|
||||||
|
if err := router.Run(":" + cfg.Port); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port string
|
||||||
|
MongoDBURI string
|
||||||
|
DBName string
|
||||||
|
JWTSecret string
|
||||||
|
Environment string
|
||||||
|
CertsPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Port: getEnv("PORT", "8080"),
|
||||||
|
MongoDBURI: getEnv("MONGODB_URI", "mongodb://localhost:27017"),
|
||||||
|
DBName: getEnv("DB_NAME", "pki_db"),
|
||||||
|
JWTSecret: getEnv("JWT_SECRET", "your-secret-key"),
|
||||||
|
Environment: getEnv("ENVIRONMENT", "development"),
|
||||||
|
CertsPath: getEnv("CERTS_PATH", "./certs"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mongodb:
|
||||||
|
image: mongo:latest
|
||||||
|
container_name: pki-mongodb
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: admin
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: admin123
|
||||||
|
MONGO_INITDB_DATABASE: pki_db
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
- ./scripts/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro
|
||||||
|
|
||||||
|
pki-api:
|
||||||
|
build: .
|
||||||
|
container_name: pki-api
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- mongodb
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
MONGODB_URI: mongodb://admin:admin123@mongodb:27017/pki_db?authSource=admin
|
||||||
|
JWT_SECRET: your-super-secret-jwt-key-change-this
|
||||||
|
volumes:
|
||||||
|
- ./certs:/app/certs
|
||||||
|
- ./internal/web/static:/app/internal/web/static
|
||||||
|
- ./internal/web/templates:/app/internal/web/templates
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
module pki-manager
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
go.mongodb.org/mongo-driver v1.12.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // 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-20201027041543-1326539a0a0a // indirect
|
||||||
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.14.0 // indirect
|
||||||
|
golang.org/x/net v0.10.0 // indirect
|
||||||
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
golang.org/x/text v0.13.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
|
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-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
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/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
|
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/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
|
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/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
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/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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
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=
|
||||||
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
|
||||||
|
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
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=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
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/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|
@ -0,0 +1,380 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"pki-manager/internal/models"
|
||||||
|
"pki-manager/internal/repository"
|
||||||
|
"pki-manager/internal/services"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handlers struct {
|
||||||
|
repo repository.Repository
|
||||||
|
cryptoService *services.CryptoService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandlers(repo repository.Repository, cryptoService *services.CryptoService) *Handlers {
|
||||||
|
return &Handlers{
|
||||||
|
repo: repo,
|
||||||
|
cryptoService: cryptoService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CA Handlers
|
||||||
|
func (h *Handlers) CreateCA(c *gin.Context) {
|
||||||
|
var req models.CreateCARequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ca, err := h.cryptoService.GenerateRootCA(req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.CreateCA(c.Request.Context(), ca); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, ca)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) GetCA(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
ca, err := h.repo.GetCA(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "CA not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't expose private key in GET requests
|
||||||
|
ca.PrivateKey = ""
|
||||||
|
c.JSON(http.StatusOK, ca)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCAs - Retourne toujours un tableau, même vide
|
||||||
|
func (h *Handlers) GetAllCAs(c *gin.Context) {
|
||||||
|
cas, err := h.repo.GetAllCAs(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
// IMPORTANT: Retourner un tableau vide, pas une erreur
|
||||||
|
c.JSON(http.StatusOK, []interface{}{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove private keys from response
|
||||||
|
for _, ca := range cas {
|
||||||
|
ca.PrivateKey = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// S'assurer qu'on retourne toujours un tableau
|
||||||
|
if cas == nil {
|
||||||
|
c.JSON(http.StatusOK, []interface{}{})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, cas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) UpdateCA(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var req models.UpdateCARequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := make(map[string]interface{})
|
||||||
|
if req.Name != "" {
|
||||||
|
updates["name"] = req.Name
|
||||||
|
}
|
||||||
|
if req.Organization != "" {
|
||||||
|
updates["organization"] = req.Organization
|
||||||
|
}
|
||||||
|
if req.Email != "" {
|
||||||
|
updates["email"] = req.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.UpdateCA(c.Request.Context(), id, updates); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "CA updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) DeleteCA(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.repo.DeleteCA(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "CA deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubCA Handlers
|
||||||
|
func (h *Handlers) CreateSubCA(c *gin.Context) {
|
||||||
|
var req models.CreateSubCARequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent CA
|
||||||
|
parentCA, err := h.repo.GetCA(c.Request.Context(), req.ParentCAID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Parent CA not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subca, err := h.cryptoService.GenerateSubCA(req, parentCA)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.CreateSubCA(c.Request.Context(), subca); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, subca)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) GetSubCA(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
subca, err := h.repo.GetSubCA(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "SubCA not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subca.PrivateKey = ""
|
||||||
|
c.JSON(http.StatusOK, subca)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSubCAs - Retourne toujours un tableau, même vide
|
||||||
|
func (h *Handlers) GetAllSubCAs(c *gin.Context) {
|
||||||
|
subcas, err := h.repo.GetAllSubCAs(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
// IMPORTANT: Retourner un tableau vide, pas une erreur
|
||||||
|
c.JSON(http.StatusOK, []interface{}{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove private keys from response
|
||||||
|
for _, subca := range subcas {
|
||||||
|
subca.PrivateKey = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// S'assurer qu'on retourne toujours un tableau
|
||||||
|
if subcas == nil {
|
||||||
|
c.JSON(http.StatusOK, []interface{}{})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, subcas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) UpdateSubCA(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var req models.UpdateSubCARequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := make(map[string]interface{})
|
||||||
|
if req.Name != "" {
|
||||||
|
updates["name"] = req.Name
|
||||||
|
}
|
||||||
|
if req.Organization != "" {
|
||||||
|
updates["organization"] = req.Organization
|
||||||
|
}
|
||||||
|
if req.Email != "" {
|
||||||
|
updates["email"] = req.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.UpdateSubCA(c.Request.Context(), id, updates); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "SubCA updated successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) DeleteSubCA(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.repo.DeleteSubCA(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "SubCA deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate Handlers
|
||||||
|
func (h *Handlers) CreateCertificate(c *gin.Context) {
|
||||||
|
var req models.CreateCertificateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get issuer as CA first
|
||||||
|
issuer, err := h.repo.GetCA(c.Request.Context(), req.IssuerCAID)
|
||||||
|
if err != nil {
|
||||||
|
// If not found as CA, try as SubCA
|
||||||
|
issuer, err := h.repo.GetSubCA(c.Request.Context(), req.IssuerCAID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Issuer not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cert, err := h.cryptoService.GenerateCertificate(req, issuer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.repo.CreateCertificate(c.Request.Context(), cert); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, cert)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := h.cryptoService.GenerateCertificate(req, issuer)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.CreateCertificate(c.Request.Context(), cert); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) GetCertificate(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
cert, err := h.repo.GetCertificate(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Certificate not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cert.PrivateKey = ""
|
||||||
|
c.JSON(http.StatusOK, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCertificates - Retourne toujours un tableau, même vide
|
||||||
|
func (h *Handlers) GetAllCertificates(c *gin.Context) {
|
||||||
|
certs, err := h.repo.GetAllCertificates(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
// IMPORTANT: Retourner un tableau vide, pas une erreur
|
||||||
|
c.JSON(http.StatusOK, []interface{}{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove private keys from response
|
||||||
|
for _, cert := range certs {
|
||||||
|
cert.PrivateKey = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// S'assurer qu'on retourne toujours un tableau
|
||||||
|
if certs == nil {
|
||||||
|
c.JSON(http.StatusOK, []interface{}{})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, certs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) DeleteCertificate(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := h.repo.DeleteCertificate(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Certificate deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) RevokeCertificate(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
var req models.RevokeCertificateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.RevokeCertificate(c.Request.Context(), id, req.Reason); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Certificate revoked successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download handlers for CA and SubCA
|
||||||
|
func (h *Handlers) DownloadCACertificate(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
ca, err := h.repo.GetCA(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "CA not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "application/x-pem-file")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+ca.CommonName+".crt")
|
||||||
|
c.String(http.StatusOK, ca.Certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) DownloadSubCACertificate(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
subca, err := h.repo.GetSubCA(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "SubCA not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "application/x-pem-file")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+subca.CommonName+".crt")
|
||||||
|
c.String(http.StatusOK, subca.Certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download handlers
|
||||||
|
func (h *Handlers) DownloadCertificate(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
cert, err := h.repo.GetCertificate(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Certificate not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "application/x-pem-file")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+cert.CommonName+".crt")
|
||||||
|
c.String(http.StatusOK, cert.Certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) DownloadPrivateKey(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
cert, err := h.repo.GetCertificate(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Certificate not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "application/x-pem-file")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+cert.CommonName+".key")
|
||||||
|
c.String(http.StatusOK, cert.PrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web Interface
|
||||||
|
func (h *Handlers) ServeWebInterface(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "index.html", nil)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"pki-manager/config"
|
||||||
|
"pki-manager/internal/repository"
|
||||||
|
"pki-manager/internal/services"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupRoutes(router *gin.Engine, repo repository.Repository, jwtSecret string) {
|
||||||
|
cfg := config.LoadConfig()
|
||||||
|
cryptoService := services.NewCryptoService(cfg.CertsPath)
|
||||||
|
handlers := NewHandlers(repo, cryptoService)
|
||||||
|
|
||||||
|
// Add logging middleware
|
||||||
|
router.Use(func(c *gin.Context) {
|
||||||
|
log.Printf("[API] %s %s", c.Request.Method, c.Request.URL.Path)
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load HTML templates
|
||||||
|
router.LoadHTMLGlob("internal/web/templates/*")
|
||||||
|
router.Static("/static", "internal/web/static")
|
||||||
|
|
||||||
|
// Web interface
|
||||||
|
router.GET("/", handlers.ServeWebInterface)
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
api := router.Group("/api/v1")
|
||||||
|
{
|
||||||
|
// CA routes
|
||||||
|
ca := api.Group("/cas")
|
||||||
|
{
|
||||||
|
ca.POST("/", handlers.CreateCA)
|
||||||
|
ca.GET("/", handlers.GetAllCAs)
|
||||||
|
ca.GET("/:id", handlers.GetCA)
|
||||||
|
ca.PUT("/:id", handlers.UpdateCA)
|
||||||
|
ca.DELETE("/:id", handlers.DeleteCA)
|
||||||
|
ca.GET("/:id/download/cert", handlers.DownloadCACertificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubCA routes
|
||||||
|
subca := api.Group("/subcas")
|
||||||
|
{
|
||||||
|
subca.POST("/", handlers.CreateSubCA)
|
||||||
|
subca.GET("/", handlers.GetAllSubCAs)
|
||||||
|
subca.GET("/:id", handlers.GetSubCA)
|
||||||
|
subca.PUT("/:id", handlers.UpdateSubCA)
|
||||||
|
subca.DELETE("/:id", handlers.DeleteSubCA)
|
||||||
|
subca.GET("/:id/download/cert", handlers.DownloadSubCACertificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate routes
|
||||||
|
cert := api.Group("/certificates")
|
||||||
|
{
|
||||||
|
cert.POST("/", handlers.CreateCertificate)
|
||||||
|
cert.GET("/", handlers.GetAllCertificates)
|
||||||
|
cert.GET("/:id", handlers.GetCertificate)
|
||||||
|
cert.DELETE("/:id", handlers.DeleteCertificate)
|
||||||
|
cert.POST("/:id/revoke", handlers.RevokeCertificate)
|
||||||
|
cert.GET("/:id/download/cert", handlers.DownloadCertificate)
|
||||||
|
cert.GET("/:id/download/key", handlers.DownloadPrivateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CA struct {
|
||||||
|
ID string `json:"id" bson:"_id"`
|
||||||
|
Name string `json:"name" bson:"name"`
|
||||||
|
CommonName string `json:"common_name" bson:"common_name"`
|
||||||
|
Organization string `json:"organization" bson:"organization"`
|
||||||
|
Country string `json:"country" bson:"country"`
|
||||||
|
Province string `json:"province" bson:"province"`
|
||||||
|
Locality string `json:"locality" bson:"locality"`
|
||||||
|
Email string `json:"email" bson:"email"`
|
||||||
|
PrivateKey string `json:"private_key,omitempty" bson:"private_key"`
|
||||||
|
Certificate string `json:"certificate" bson:"certificate"`
|
||||||
|
SerialNumber string `json:"serial_number" bson:"serial_number"`
|
||||||
|
ValidFrom time.Time `json:"valid_from" bson:"valid_from"`
|
||||||
|
ValidTo time.Time `json:"valid_to" bson:"valid_to"`
|
||||||
|
IsRoot bool `json:"is_root" bson:"is_root"`
|
||||||
|
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCARequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
CommonName string `json:"common_name" binding:"required"`
|
||||||
|
Organization string `json:"organization" binding:"required"`
|
||||||
|
Country string `json:"country" binding:"required"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
Locality string `json:"locality"`
|
||||||
|
Email string `json:"email" binding:"omitempty,email"` // omitempty permet les chaînes vides
|
||||||
|
KeySize int `json:"key_size" binding:"required,min=2048"`
|
||||||
|
ValidYears int `json:"valid_years" binding:"required,min=1,max=20"`
|
||||||
|
IsRoot bool `json:"is_root"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateCARequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Certificate struct {
|
||||||
|
ID string `json:"id" bson:"_id"`
|
||||||
|
CommonName string `json:"common_name" bson:"common_name"`
|
||||||
|
Subject string `json:"subject" bson:"subject"`
|
||||||
|
DNSNames []string `json:"dns_names" bson:"dns_names"`
|
||||||
|
IPAddresses []string `json:"ip_addresses" bson:"ip_addresses"`
|
||||||
|
Type string `json:"type" bson:"type"` // "server" or "client"
|
||||||
|
PrivateKey string `json:"private_key,omitempty" bson:"private_key"`
|
||||||
|
Certificate string `json:"certificate" bson:"certificate"`
|
||||||
|
SerialNumber string `json:"serial_number" bson:"serial_number"`
|
||||||
|
ValidFrom time.Time `json:"valid_from" bson:"valid_from"`
|
||||||
|
ValidTo time.Time `json:"valid_to" bson:"valid_to"`
|
||||||
|
IssuerCAID string `json:"issuer_ca_id" bson:"issuer_ca_id"`
|
||||||
|
Revoked bool `json:"revoked" bson:"revoked"`
|
||||||
|
RevokedAt time.Time `json:"revoked_at,omitempty" bson:"revoked_at"`
|
||||||
|
RevokedReason string `json:"revoked_reason,omitempty" bson:"revoked_reason"`
|
||||||
|
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCertificateRequest struct {
|
||||||
|
CommonName string `json:"common_name" binding:"required"`
|
||||||
|
DNSNames []string `json:"dns_names"`
|
||||||
|
IPAddresses []string `json:"ip_addresses"`
|
||||||
|
Type string `json:"type" binding:"required,oneof=server client"`
|
||||||
|
KeySize int `json:"key_size" binding:"required,min=2048"`
|
||||||
|
ValidDays int `json:"valid_days" binding:"required,min=1,max=365"`
|
||||||
|
IssuerCAID string `json:"issuer_ca_id" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RevokeCertificateRequest struct {
|
||||||
|
Reason string `json:"reason" binding:"required"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubCA struct {
|
||||||
|
ID string `json:"id" bson:"_id"`
|
||||||
|
Name string `json:"name" bson:"name"`
|
||||||
|
CommonName string `json:"common_name" bson:"common_name"`
|
||||||
|
Organization string `json:"organization" bson:"organization"`
|
||||||
|
Country string `json:"country" bson:"country"`
|
||||||
|
Province string `json:"province" bson:"province"`
|
||||||
|
Locality string `json:"locality" bson:"locality"`
|
||||||
|
Email string `json:"email" bson:"email"`
|
||||||
|
PrivateKey string `json:"private_key,omitempty" bson:"private_key"`
|
||||||
|
Certificate string `json:"certificate" bson:"certificate"`
|
||||||
|
SerialNumber string `json:"serial_number" bson:"serial_number"`
|
||||||
|
ValidFrom time.Time `json:"valid_from" bson:"valid_from"`
|
||||||
|
ValidTo time.Time `json:"valid_to" bson:"valid_to"`
|
||||||
|
ParentCAID string `json:"parent_ca_id" bson:"parent_ca_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateSubCARequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
CommonName string `json:"common_name" binding:"required"`
|
||||||
|
Organization string `json:"organization" binding:"required"`
|
||||||
|
Country string `json:"country" binding:"required"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
Locality string `json:"locality"`
|
||||||
|
Email string `json:"email" binding:"omitempty,email"` // omitempty permet les chaînes vides
|
||||||
|
KeySize int `json:"key_size" binding:"required,min=2048"`
|
||||||
|
ValidYears int `json:"valid_years" binding:"required,min=1,max=10"`
|
||||||
|
ParentCAID string `json:"parent_ca_id" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSubCARequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"pki-manager/internal/models"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MongoRepository struct {
|
||||||
|
db *mongo.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMongoRepository(db *mongo.Database) *MongoRepository {
|
||||||
|
return &MongoRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) CreateCA(ctx context.Context, ca *models.CA) error {
|
||||||
|
ca.CreatedAt = time.Now()
|
||||||
|
ca.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
_, err := r.db.Collection("cas").InsertOne(ctx, ca)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) GetCA(ctx context.Context, id string) (*models.CA, error) {
|
||||||
|
var ca models.CA
|
||||||
|
err := r.db.Collection("cas").FindOne(ctx, bson.M{"_id": id}).Decode(&ca)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ca, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) GetAllCAs(ctx context.Context) ([]*models.CA, error) {
|
||||||
|
var cas []*models.CA
|
||||||
|
cursor, err := r.db.Collection("cas").Find(ctx, bson.M{})
|
||||||
|
if err != nil {
|
||||||
|
// Retourner un tableau vide au lieu d'une erreur
|
||||||
|
return []*models.CA{}, nil
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var ca models.CA
|
||||||
|
if err := cursor.Decode(&ca); err != nil {
|
||||||
|
continue // Ignorer les erreurs de décodage
|
||||||
|
}
|
||||||
|
cas = append(cas, &ca)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) UpdateCA(ctx context.Context, id string, updates map[string]interface{}) error {
|
||||||
|
updates["updated_at"] = time.Now()
|
||||||
|
_, err := r.db.Collection("cas").UpdateOne(
|
||||||
|
ctx,
|
||||||
|
bson.M{"_id": id},
|
||||||
|
bson.M{"$set": updates},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) DeleteCA(ctx context.Context, id string) error {
|
||||||
|
_, err := r.db.Collection("cas").DeleteOne(ctx, bson.M{"_id": id})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubCA methods
|
||||||
|
func (r *MongoRepository) CreateSubCA(ctx context.Context, subca *models.SubCA) error {
|
||||||
|
subca.CreatedAt = time.Now()
|
||||||
|
subca.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
_, err := r.db.Collection("subcas").InsertOne(ctx, subca)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) GetSubCA(ctx context.Context, id string) (*models.SubCA, error) {
|
||||||
|
var subca models.SubCA
|
||||||
|
err := r.db.Collection("subcas").FindOne(ctx, bson.M{"_id": id}).Decode(&subca)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &subca, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) GetSubCAsByParent(ctx context.Context, parentCAID string) ([]*models.SubCA, error) {
|
||||||
|
var subcas []*models.SubCA
|
||||||
|
cursor, err := r.db.Collection("subcas").Find(ctx, bson.M{"parent_ca_id": parentCAID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var subca models.SubCA
|
||||||
|
if err := cursor.Decode(&subca); err != nil {
|
||||||
|
log.Println("Error decoding SubCA:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subcas = append(subcas, &subca)
|
||||||
|
}
|
||||||
|
return subcas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) GetAllSubCAs(ctx context.Context) ([]*models.SubCA, error) {
|
||||||
|
var subcas []*models.SubCA
|
||||||
|
cursor, err := r.db.Collection("subcas").Find(ctx, bson.M{})
|
||||||
|
if err != nil {
|
||||||
|
// Retourner un tableau vide au lieu d'une erreur
|
||||||
|
return []*models.SubCA{}, nil
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var subca models.SubCA
|
||||||
|
if err := cursor.Decode(&subca); err != nil {
|
||||||
|
continue // Ignorer les erreurs de décodage
|
||||||
|
}
|
||||||
|
subcas = append(subcas, &subca)
|
||||||
|
}
|
||||||
|
|
||||||
|
return subcas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) UpdateSubCA(ctx context.Context, id string, updates map[string]interface{}) error {
|
||||||
|
updates["updated_at"] = time.Now()
|
||||||
|
_, err := r.db.Collection("subcas").UpdateOne(
|
||||||
|
ctx,
|
||||||
|
bson.M{"_id": id},
|
||||||
|
bson.M{"$set": updates},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) DeleteSubCA(ctx context.Context, id string) error {
|
||||||
|
_, err := r.db.Collection("subcas").DeleteOne(ctx, bson.M{"_id": id})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate methods
|
||||||
|
func (r *MongoRepository) CreateCertificate(ctx context.Context, cert *models.Certificate) error {
|
||||||
|
cert.CreatedAt = time.Now()
|
||||||
|
|
||||||
|
_, err := r.db.Collection("certificates").InsertOne(ctx, cert)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) GetCertificate(ctx context.Context, id string) (*models.Certificate, error) {
|
||||||
|
var cert models.Certificate
|
||||||
|
err := r.db.Collection("certificates").FindOne(ctx, bson.M{"_id": id}).Decode(&cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) GetCertificatesByIssuer(ctx context.Context, issuerCAID string) ([]*models.Certificate, error) {
|
||||||
|
var certs []*models.Certificate
|
||||||
|
cursor, err := r.db.Collection("certificates").Find(ctx, bson.M{"issuer_ca_id": issuerCAID})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var cert models.Certificate
|
||||||
|
if err := cursor.Decode(&cert); err != nil {
|
||||||
|
log.Println("Error decoding certificate:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
certs = append(certs, &cert)
|
||||||
|
}
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) GetAllCertificates(ctx context.Context) ([]*models.Certificate, error) {
|
||||||
|
var certs []*models.Certificate
|
||||||
|
cursor, err := r.db.Collection("certificates").Find(ctx, bson.M{})
|
||||||
|
if err != nil {
|
||||||
|
// Retourner un tableau vide au lieu d'une erreur
|
||||||
|
return []*models.Certificate{}, nil
|
||||||
|
}
|
||||||
|
defer cursor.Close(ctx)
|
||||||
|
|
||||||
|
for cursor.Next(ctx) {
|
||||||
|
var cert models.Certificate
|
||||||
|
if err := cursor.Decode(&cert); err != nil {
|
||||||
|
continue // Ignorer les erreurs de décodage
|
||||||
|
}
|
||||||
|
certs = append(certs, &cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) UpdateCertificate(ctx context.Context, id string, updates map[string]interface{}) error {
|
||||||
|
_, err := r.db.Collection("certificates").UpdateOne(
|
||||||
|
ctx,
|
||||||
|
bson.M{"_id": id},
|
||||||
|
bson.M{"$set": updates},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) DeleteCertificate(ctx context.Context, id string) error {
|
||||||
|
_, err := r.db.Collection("certificates").DeleteOne(ctx, bson.M{"_id": id})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MongoRepository) RevokeCertificate(ctx context.Context, id string, reason string) error {
|
||||||
|
updates := bson.M{
|
||||||
|
"revoked": true,
|
||||||
|
"revoked_at": time.Now(),
|
||||||
|
"revoked_reason": reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := r.db.Collection("certificates").UpdateOne(
|
||||||
|
ctx,
|
||||||
|
bson.M{"_id": id},
|
||||||
|
bson.M{"$set": updates},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"pki-manager/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository interface {
|
||||||
|
// CA operations
|
||||||
|
CreateCA(ctx context.Context, ca *models.CA) error
|
||||||
|
GetCA(ctx context.Context, id string) (*models.CA, error)
|
||||||
|
GetAllCAs(ctx context.Context) ([]*models.CA, error)
|
||||||
|
UpdateCA(ctx context.Context, id string, updates map[string]interface{}) error
|
||||||
|
DeleteCA(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
// SubCA operations
|
||||||
|
CreateSubCA(ctx context.Context, subca *models.SubCA) error
|
||||||
|
GetSubCA(ctx context.Context, id string) (*models.SubCA, error)
|
||||||
|
GetSubCAsByParent(ctx context.Context, parentCAID string) ([]*models.SubCA, error)
|
||||||
|
GetAllSubCAs(ctx context.Context) ([]*models.SubCA, error)
|
||||||
|
UpdateSubCA(ctx context.Context, id string, updates map[string]interface{}) error
|
||||||
|
DeleteSubCA(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
// Certificate operations
|
||||||
|
CreateCertificate(ctx context.Context, cert *models.Certificate) error
|
||||||
|
GetCertificate(ctx context.Context, id string) (*models.Certificate, error)
|
||||||
|
GetCertificatesByIssuer(ctx context.Context, issuerCAID string) ([]*models.Certificate, error)
|
||||||
|
GetAllCertificates(ctx context.Context) ([]*models.Certificate, error)
|
||||||
|
UpdateCertificate(ctx context.Context, id string, updates map[string]interface{}) error
|
||||||
|
DeleteCertificate(ctx context.Context, id string) error
|
||||||
|
RevokeCertificate(ctx context.Context, id string, reason string) error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,327 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"pki-manager/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CryptoService struct {
|
||||||
|
certsPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCryptoService(certsPath string) *CryptoService {
|
||||||
|
return &CryptoService{certsPath: certsPath}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CryptoService) GenerateRootCA(req models.CreateCARequest) (*models.CA, error) {
|
||||||
|
// Generate private key
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, req.KeySize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certificate template
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate serial number: %v", err)
|
||||||
|
}
|
||||||
|
// Préparer le sujet avec email optionnel
|
||||||
|
subject := pkix.Name{
|
||||||
|
CommonName: req.CommonName,
|
||||||
|
Organization: []string{req.Organization},
|
||||||
|
Country: []string{req.Country},
|
||||||
|
Province: []string{req.Province},
|
||||||
|
Locality: []string{req.Locality},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter l'email seulement s'il est fourni
|
||||||
|
if req.Email != "" {
|
||||||
|
// L'email n'est pas un champ standard dans pkix.Name
|
||||||
|
// On le stockera dans les champs personnalisés
|
||||||
|
}
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: subject,
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().AddDate(req.ValidYears, 0, 0),
|
||||||
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
IsCA: true,
|
||||||
|
MaxPathLen: 0,
|
||||||
|
MaxPathLenZero: true,
|
||||||
|
}
|
||||||
|
// Ajouter l'email seulement s'il est fourni
|
||||||
|
if req.Email != "" {
|
||||||
|
// L'email n'est pas un champ standard dans pkix.Name
|
||||||
|
// On le stockera dans les champs personnalisés
|
||||||
|
}
|
||||||
|
// Self-sign the certificate
|
||||||
|
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode private key
|
||||||
|
privBytes := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
privPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: privBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Encode certificate
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
ca := &models.CA{
|
||||||
|
ID: fmt.Sprintf("ca_%d", time.Now().UnixNano()),
|
||||||
|
Name: req.Name,
|
||||||
|
CommonName: req.CommonName,
|
||||||
|
Organization: req.Organization,
|
||||||
|
Country: req.Country,
|
||||||
|
Province: req.Province,
|
||||||
|
Locality: req.Locality,
|
||||||
|
Email: req.Email,
|
||||||
|
PrivateKey: string(privPEM),
|
||||||
|
Certificate: string(certPEM),
|
||||||
|
SerialNumber: serialNumber.String(),
|
||||||
|
ValidFrom: template.NotBefore,
|
||||||
|
ValidTo: template.NotAfter,
|
||||||
|
IsRoot: req.IsRoot,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ca, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CryptoService) GenerateSubCA(req models.CreateSubCARequest, parentCA *models.CA) (*models.SubCA, error) {
|
||||||
|
// Parse parent CA certificate and private key
|
||||||
|
parentCertBlock, _ := pem.Decode([]byte(parentCA.Certificate))
|
||||||
|
if parentCertBlock == nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse parent CA certificate")
|
||||||
|
}
|
||||||
|
parentCert, err := x509.ParseCertificate(parentCertBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse parent CA certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentKeyBlock, _ := pem.Decode([]byte(parentCA.PrivateKey))
|
||||||
|
if parentKeyBlock == nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse parent CA private key")
|
||||||
|
}
|
||||||
|
parentKey, err := x509.ParsePKCS1PrivateKey(parentKeyBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse parent CA private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate subCA private key
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, req.KeySize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certificate template
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate serial number: %v", err)
|
||||||
|
}
|
||||||
|
subject := pkix.Name{
|
||||||
|
CommonName: req.CommonName,
|
||||||
|
Organization: []string{req.Organization},
|
||||||
|
Country: []string{req.Country},
|
||||||
|
Province: []string{req.Province},
|
||||||
|
Locality: []string{req.Locality},
|
||||||
|
}
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: subject,
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().AddDate(req.ValidYears, 0, 0),
|
||||||
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
IsCA: true,
|
||||||
|
MaxPathLen: 0,
|
||||||
|
MaxPathLenZero: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the certificate with parent CA
|
||||||
|
certBytes, err := x509.CreateCertificate(rand.Reader, &template, parentCert, &priv.PublicKey, parentKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode private key
|
||||||
|
privBytes := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
privPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: privBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Encode certificate
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
subca := &models.SubCA{
|
||||||
|
ID: fmt.Sprintf("subca_%d", time.Now().UnixNano()),
|
||||||
|
Name: req.Name,
|
||||||
|
CommonName: req.CommonName,
|
||||||
|
Organization: req.Organization,
|
||||||
|
Country: req.Country,
|
||||||
|
Province: req.Province,
|
||||||
|
Locality: req.Locality,
|
||||||
|
Email: req.Email,
|
||||||
|
PrivateKey: string(privPEM),
|
||||||
|
Certificate: string(certPEM),
|
||||||
|
SerialNumber: serialNumber.String(),
|
||||||
|
ValidFrom: template.NotBefore,
|
||||||
|
ValidTo: template.NotAfter,
|
||||||
|
ParentCAID: req.ParentCAID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return subca, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CryptoService) GenerateCertificate(req models.CreateCertificateRequest, issuer interface{}) (*models.Certificate, error) {
|
||||||
|
var issuerCert *x509.Certificate
|
||||||
|
var issuerKey *rsa.PrivateKey
|
||||||
|
|
||||||
|
// Parse issuer based on type
|
||||||
|
switch iss := issuer.(type) {
|
||||||
|
case *models.CA:
|
||||||
|
certBlock, _ := pem.Decode([]byte(iss.Certificate))
|
||||||
|
if certBlock == nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse issuer certificate")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
issuerCert, err = x509.ParseCertificate(certBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse issuer certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBlock, _ := pem.Decode([]byte(iss.PrivateKey))
|
||||||
|
if keyBlock == nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse issuer private key")
|
||||||
|
}
|
||||||
|
issuerKey, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse issuer private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case *models.SubCA:
|
||||||
|
certBlock, _ := pem.Decode([]byte(iss.Certificate))
|
||||||
|
if certBlock == nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse issuer certificate")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
issuerCert, err = x509.ParseCertificate(certBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse issuer certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBlock, _ := pem.Decode([]byte(iss.PrivateKey))
|
||||||
|
if keyBlock == nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse issuer private key")
|
||||||
|
}
|
||||||
|
issuerKey, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse issuer private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported issuer type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate certificate private key
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, req.KeySize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create certificate template
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate serial number: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: req.CommonName,
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().AddDate(0, 0, req.ValidDays),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
ExtKeyUsage: s.getExtKeyUsage(req.Type),
|
||||||
|
DNSNames: req.DNSNames,
|
||||||
|
IPAddresses: s.parseIPs(req.IPAddresses),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the certificate
|
||||||
|
certBytes, err := x509.CreateCertificate(rand.Reader, &template, issuerCert, &priv.PublicKey, issuerKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode private key
|
||||||
|
privBytes := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
privPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: privBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Encode certificate
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
cert := &models.Certificate{
|
||||||
|
ID: fmt.Sprintf("cert_%d", time.Now().UnixNano()),
|
||||||
|
CommonName: req.CommonName,
|
||||||
|
Subject: template.Subject.String(),
|
||||||
|
DNSNames: req.DNSNames,
|
||||||
|
IPAddresses: req.IPAddresses,
|
||||||
|
Type: req.Type,
|
||||||
|
PrivateKey: string(privPEM),
|
||||||
|
Certificate: string(certPEM),
|
||||||
|
SerialNumber: serialNumber.String(),
|
||||||
|
ValidFrom: template.NotBefore,
|
||||||
|
ValidTo: template.NotAfter,
|
||||||
|
IssuerCAID: req.IssuerCAID,
|
||||||
|
Revoked: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CryptoService) getExtKeyUsage(certType string) []x509.ExtKeyUsage {
|
||||||
|
switch certType {
|
||||||
|
case "server":
|
||||||
|
return []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
|
||||||
|
case "client":
|
||||||
|
return []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
|
||||||
|
default:
|
||||||
|
return []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CryptoService) parseIPs(ips []string) []net.IP {
|
||||||
|
var parsedIPs []net.IP
|
||||||
|
for _, ipStr := range ips {
|
||||||
|
if ip := net.ParseIP(ipStr); ip != nil {
|
||||||
|
parsedIPs = append(parsedIPs, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedIPs
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,502 @@
|
||||||
|
/* Ajoutez ces styles à la fin du fichier CSS existant */
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-activity {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-activity h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container .btn {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner styles */
|
||||||
|
.fa-spinner {
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal improvements */
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form improvements */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #4a5568;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus,
|
||||||
|
.form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button improvements */
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: linear-gradient(135deg, #c53030 0%, #9b2c2c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close button */
|
||||||
|
.close-btn {
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #a0aec0;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 20px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge styles */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-server {
|
||||||
|
background-color: #4299e1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-client {
|
||||||
|
background-color: #48bb78;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background-color: #ed8936;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail view styles */
|
||||||
|
.detail-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-group:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4a5568;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-group span {
|
||||||
|
display: block;
|
||||||
|
color: #2d3748;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monospace {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-preview {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.certificate-preview label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-pem {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
resize: vertical;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.loading-spinner {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner i {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner p {
|
||||||
|
color: #718096;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip for buttons */
|
||||||
|
button[title] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[title]:hover::after {
|
||||||
|
content: attr(title);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
z-index: 1000;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading states */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #e53e3e;
|
||||||
|
background: #fed7d7;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section headers */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table improvements */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background: #f7fafc;
|
||||||
|
padding: 12px 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4a5568;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tr:hover {
|
||||||
|
background: #f7fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badges */
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.valid {
|
||||||
|
background: #c6f6d5;
|
||||||
|
color: #22543d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.revoked {
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #742a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.expired {
|
||||||
|
background: #feebc8;
|
||||||
|
color: #744210;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button styles */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary:hover {
|
||||||
|
background: #5a67d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.small {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger {
|
||||||
|
background: #fc8181;
|
||||||
|
color: #742a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.danger:hover {
|
||||||
|
background: #f56565;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.warning {
|
||||||
|
background: #ed8936;
|
||||||
|
color: #744210;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.warning:hover {
|
||||||
|
background: #dd6b20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal animations */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,887 @@
|
||||||
|
class PKIManager {
|
||||||
|
constructor() {
|
||||||
|
this.apiBase = '/api/v1';
|
||||||
|
this.currentTab = 'dashboard';
|
||||||
|
this.cas = [];
|
||||||
|
this.subcas = [];
|
||||||
|
this.certificates = [];
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.showTab('dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Tab navigation
|
||||||
|
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const tab = e.target.dataset.tab || e.target.closest('.nav-btn').dataset.tab;
|
||||||
|
this.showTab(tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submissions
|
||||||
|
const caForm = document.getElementById('createCAForm');
|
||||||
|
const subcaForm = document.getElementById('createSubCAForm');
|
||||||
|
const certForm = document.getElementById('createCertForm');
|
||||||
|
|
||||||
|
if (caForm) caForm.addEventListener('submit', (e) => this.handleCreateCA(e));
|
||||||
|
if (subcaForm) subcaForm.addEventListener('submit', (e) => this.handleCreateSubCA(e));
|
||||||
|
if (certForm) certForm.addEventListener('submit', (e) => this.handleCreateCertificate(e));
|
||||||
|
|
||||||
|
// Close modal buttons
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('close-btn') || e.target.closest('.close-btn')) {
|
||||||
|
this.hideModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click outside modal
|
||||||
|
window.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('modal')) {
|
||||||
|
this.hideModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showTab(tabName) {
|
||||||
|
console.log(`Showing tab: ${tabName}`);
|
||||||
|
|
||||||
|
// Hide all tabs
|
||||||
|
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||||
|
tab.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update active button
|
||||||
|
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||||||
|
if (activeBtn) activeBtn.classList.add('active');
|
||||||
|
|
||||||
|
// Show selected tab
|
||||||
|
const tabElement = document.getElementById(tabName + 'Tab');
|
||||||
|
if (tabElement) {
|
||||||
|
tabElement.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentTab = tabName;
|
||||||
|
|
||||||
|
// Load data for tab
|
||||||
|
setTimeout(() => this.loadTabData(tabName), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTabData(tabName) {
|
||||||
|
switch(tabName) {
|
||||||
|
case 'dashboard':
|
||||||
|
await this.loadDashboard();
|
||||||
|
break;
|
||||||
|
case 'cas':
|
||||||
|
await this.loadCAs();
|
||||||
|
break;
|
||||||
|
case 'subcas':
|
||||||
|
await this.loadSubCAs();
|
||||||
|
break;
|
||||||
|
case 'certificates':
|
||||||
|
await this.loadCertificates();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadDashboard() {
|
||||||
|
const content = document.getElementById('dashboardContent');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
content.innerHTML = '<div class="loading">Loading dashboard...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Charger les données
|
||||||
|
await this.fetchAllData();
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Root CAs</h3>
|
||||||
|
<p class="stat-number">${this.cas.filter(c => c.is_root).length}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Sub CAs</h3>
|
||||||
|
<p class="stat-number">${this.subcas.length}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Certificates</h3>
|
||||||
|
<p class="stat-number">${this.certificates.length}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Active</h3>
|
||||||
|
<p class="stat-number">${this.certificates.filter(c => !c.revoked && new Date(c.valid_to) > new Date()).length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="recent-activity">
|
||||||
|
<h3>Recent Certificates</h3>
|
||||||
|
${this.certificates.length > 0 ? `
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Name</th><th>Type</th><th>Issued</th><th>Expires</th><th>Status</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${this.certificates.slice(0, 5).map(cert => `
|
||||||
|
<tr>
|
||||||
|
<td>${cert.common_name}</td>
|
||||||
|
<td>${cert.type}</td>
|
||||||
|
<td>${new Date(cert.created_at).toLocaleDateString()}</td>
|
||||||
|
<td>${new Date(cert.valid_to).toLocaleDateString()}</td>
|
||||||
|
<td><span class="status ${cert.revoked ? 'revoked' : new Date(cert.valid_to) > new Date() ? 'valid' : 'expired'}">
|
||||||
|
${cert.revoked ? 'Revoked' : new Date(cert.valid_to) > new Date() ? 'Valid' : 'Expired'}
|
||||||
|
</span></td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
` : '<p>No certificates yet</p>'}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dashboard error:', error);
|
||||||
|
content.innerHTML = '<div class="error">Failed to load dashboard</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCAs() {
|
||||||
|
const content = document.getElementById('casContent');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
content.innerHTML = '<div class="loading">Loading CAs...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchCAs();
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>Certificate Authorities</h3>
|
||||||
|
<button class="btn primary" onclick="pki.showCreateCAModal()">
|
||||||
|
<i class="fas fa-plus"></i> New CA
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${this.cas.length > 0 ? `
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Common Name</th>
|
||||||
|
<th>Organization</th>
|
||||||
|
<th>Valid From</th>
|
||||||
|
<th>Valid To</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${this.cas.map(ca => `
|
||||||
|
<tr>
|
||||||
|
<td>${ca.name}</td>
|
||||||
|
<td>${ca.common_name}</td>
|
||||||
|
<td>${ca.organization}</td>
|
||||||
|
<td>${new Date(ca.valid_from).toLocaleDateString()}</td>
|
||||||
|
<td>${new Date(ca.valid_to).toLocaleDateString()}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<button class="btn small" onclick="pki.viewCA('${ca.id}')">View</button>
|
||||||
|
<button class="btn small danger" onclick="pki.deleteCA('${ca.id}')">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
` : `
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No Certificate Authorities found</p>
|
||||||
|
<button class="btn primary" onclick="pki.showCreateCAModal()">
|
||||||
|
Create your first CA
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CAs error:', error);
|
||||||
|
content.innerHTML = '<div class="error">Failed to load CAs</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSubCAs() {
|
||||||
|
const content = document.getElementById('subcasContent');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
content.innerHTML = '<div class="loading">Loading Sub CAs...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchSubCAs();
|
||||||
|
await this.fetchCAs(); // Need CAs for parent info
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>Sub Certificate Authorities</h3>
|
||||||
|
<button class="btn primary" onclick="pki.showCreateSubCAModal()">
|
||||||
|
<i class="fas fa-plus"></i> New Sub CA
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${this.subcas.length > 0 ? `
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Common Name</th>
|
||||||
|
<th>Parent CA</th>
|
||||||
|
<th>Valid From</th>
|
||||||
|
<th>Valid To</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${this.subcas.map(subca => {
|
||||||
|
const parent = this.cas.find(ca => ca.id === subca.parent_ca_id);
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${subca.name}</td>
|
||||||
|
<td>${subca.common_name}</td>
|
||||||
|
<td>${parent ? parent.name : 'Unknown'}</td>
|
||||||
|
<td>${new Date(subca.valid_from).toLocaleDateString()}</td>
|
||||||
|
<td>${new Date(subca.valid_to).toLocaleDateString()}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<button class="btn small" onclick="pki.viewSubCA('${subca.id}')">View</button>
|
||||||
|
<button class="btn small danger" onclick="pki.deleteSubCA('${subca.id}')">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
` : `
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No Sub Certificate Authorities found</p>
|
||||||
|
${this.cas.length > 0 ? `
|
||||||
|
<button class="btn primary" onclick="pki.showCreateSubCAModal()">
|
||||||
|
Create your first Sub CA
|
||||||
|
</button>
|
||||||
|
` : '<p>Create a CA first to create Sub CAs</p>'}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SubCAs error:', error);
|
||||||
|
content.innerHTML = '<div class="error">Failed to load Sub CAs</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadCertificates() {
|
||||||
|
const content = document.getElementById('certsContent');
|
||||||
|
if (!content) return;
|
||||||
|
|
||||||
|
content.innerHTML = '<div class="loading">Loading Certificates...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchCertificates();
|
||||||
|
await this.fetchCAs();
|
||||||
|
await this.fetchSubCAs();
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>Certificates</h3>
|
||||||
|
<button class="btn primary" onclick="pki.showCreateCertModal()">
|
||||||
|
<i class="fas fa-plus"></i> New Certificate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${this.certificates.length > 0 ? `
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Common Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Issuer</th>
|
||||||
|
<th>Issued</th>
|
||||||
|
<th>Expires</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${this.certificates.map(cert => {
|
||||||
|
const issuer = [...this.cas, ...this.subcas].find(i => i.id === cert.issuer_ca_id);
|
||||||
|
const status = cert.revoked ? 'revoked' : new Date(cert.valid_to) > new Date() ? 'valid' : 'expired';
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${cert.common_name}</td>
|
||||||
|
<td>${cert.type}</td>
|
||||||
|
<td>${issuer ? issuer.name : 'Unknown'}</td>
|
||||||
|
<td>${new Date(cert.created_at).toLocaleDateString()}</td>
|
||||||
|
<td>${new Date(cert.valid_to).toLocaleDateString()}</td>
|
||||||
|
<td><span class="status ${status}">${status.charAt(0).toUpperCase() + status.slice(1)}</span></td>
|
||||||
|
<td class="actions">
|
||||||
|
<button class="btn small" onclick="pki.viewCertificate('${cert.id}')">View</button>
|
||||||
|
<button class="btn small" onclick="pki.downloadCertificate('${cert.id}')">Download</button>
|
||||||
|
${!cert.revoked ? `<button class="btn small warning" onclick="pki.revokeCertificate('${cert.id}')">Revoke</button>` : ''}
|
||||||
|
<button class="btn small danger" onclick="pki.deleteCertificate('${cert.id}')">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
` : `
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No certificates found</p>
|
||||||
|
${this.cas.length > 0 || this.subcas.length > 0 ? `
|
||||||
|
<button class="btn primary" onclick="pki.showCreateCertModal()">
|
||||||
|
Create your first certificate
|
||||||
|
</button>
|
||||||
|
` : '<p>Create a CA or Sub CA first</p>'}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Certificates error:', error);
|
||||||
|
content.innerHTML = '<div class="error">Failed to load certificates</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAllData() {
|
||||||
|
try {
|
||||||
|
const [cas, subcas, certs] = await Promise.all([
|
||||||
|
this.fetchCAs(),
|
||||||
|
this.fetchSubCAs(),
|
||||||
|
this.fetchCertificates()
|
||||||
|
]);
|
||||||
|
return { cas, subcas, certs };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching all data:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCAs() {
|
||||||
|
try {
|
||||||
|
console.log('Fetching CAs from:', `${this.apiBase}/cas`);
|
||||||
|
const response = await fetch(`${this.apiBase}/cas`);
|
||||||
|
|
||||||
|
console.log('CAs response status:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn(`CAs fetch failed: ${response.status} ${response.statusText}`);
|
||||||
|
this.cas = [];
|
||||||
|
return this.cas;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('CAs raw data:', data);
|
||||||
|
|
||||||
|
// S'assurer que c'est un tableau
|
||||||
|
this.cas = Array.isArray(data) ? data : [];
|
||||||
|
console.log(`Loaded ${this.cas.length} CAs`);
|
||||||
|
|
||||||
|
return this.cas;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CAs fetch error:', error);
|
||||||
|
this.cas = [];
|
||||||
|
return this.cas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchSubCAs() {
|
||||||
|
try {
|
||||||
|
console.log('Fetching SubCAs from:', `${this.apiBase}/subcas`);
|
||||||
|
const response = await fetch(`${this.apiBase}/subcas`);
|
||||||
|
|
||||||
|
console.log('SubCAs response status:', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn(`SubCAs fetch failed: ${response.status} ${response.statusText}`);
|
||||||
|
this.subcas = [];
|
||||||
|
return this.subcas;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('SubCAs raw data:', data);
|
||||||
|
|
||||||
|
// S'assurer que c'est un tableau
|
||||||
|
this.subcas = Array.isArray(data) ? data : [];
|
||||||
|
console.log(`Loaded ${this.subcas.length} SubCAs`);
|
||||||
|
|
||||||
|
return this.subcas;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SubCAs fetch error:', error);
|
||||||
|
this.subcas = [];
|
||||||
|
return this.subcas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCertificates() {
|
||||||
|
try {
|
||||||
|
console.log('Fetching certificates...');
|
||||||
|
const response = await fetch(`${this.apiBase}/certificates`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn(`Certificates fetch failed: ${response.status}`);
|
||||||
|
this.certificates = [];
|
||||||
|
return this.certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
this.certificates = Array.isArray(data) ? data : [];
|
||||||
|
console.log(`Loaded ${this.certificates.length} certificates`);
|
||||||
|
return this.certificates;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Certificates fetch error:', error);
|
||||||
|
this.certificates = [];
|
||||||
|
return this.certificates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async showCreateCAModal() {
|
||||||
|
const modal = document.getElementById('createCAModal');
|
||||||
|
if (modal) modal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async showCreateSubCAModal() {
|
||||||
|
try {
|
||||||
|
// Charger les CAs pour le dropdown
|
||||||
|
await this.fetchCAs();
|
||||||
|
|
||||||
|
if (this.cas.length === 0) {
|
||||||
|
this.showError('Create a CA first before creating a Sub CA');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le dropdown
|
||||||
|
const dropdown = document.getElementById('parentCA');
|
||||||
|
if (dropdown) {
|
||||||
|
dropdown.innerHTML = this.cas.map(ca =>
|
||||||
|
`<option value="${ca.id}">${ca.name} (${ca.common_name})</option>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = document.getElementById('createSubCAModal');
|
||||||
|
if (modal) modal.classList.remove('hidden');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error showing SubCA modal:', error);
|
||||||
|
this.showError('Failed to load CAs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async showCreateCertModal() {
|
||||||
|
console.log('=== showCreateCertModal START ===');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Charger les données
|
||||||
|
console.log('Loading issuers...');
|
||||||
|
await this.fetchCAs();
|
||||||
|
await this.fetchSubCAs();
|
||||||
|
|
||||||
|
// 2. Vérifier les données
|
||||||
|
console.log(`CAs: ${this.cas.length} items`, this.cas);
|
||||||
|
console.log(`SubCAs: ${this.subcas.length} items`, this.subcas);
|
||||||
|
|
||||||
|
// 3. Combiner tous les émetteurs
|
||||||
|
const allIssuers = [];
|
||||||
|
|
||||||
|
// Ajouter les CAs
|
||||||
|
if (this.cas && Array.isArray(this.cas)) {
|
||||||
|
this.cas.forEach(ca => {
|
||||||
|
if (ca && ca.id) {
|
||||||
|
allIssuers.push({
|
||||||
|
id: ca.id,
|
||||||
|
name: ca.name || ca.common_name || 'Unnamed CA',
|
||||||
|
common_name: ca.common_name || 'No CN',
|
||||||
|
type: 'CA'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter les SubCAs
|
||||||
|
if (this.subcas && Array.isArray(this.subcas)) {
|
||||||
|
this.subcas.forEach(subca => {
|
||||||
|
if (subca && subca.id) {
|
||||||
|
allIssuers.push({
|
||||||
|
id: subca.id,
|
||||||
|
name: subca.name || subca.common_name || 'Unnamed SubCA',
|
||||||
|
common_name: subca.common_name || 'No CN',
|
||||||
|
type: 'SubCA'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('All issuers combined:', allIssuers);
|
||||||
|
|
||||||
|
if (allIssuers.length === 0) {
|
||||||
|
this.showError('Please create a CA or Sub CA first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Remplir le dropdown
|
||||||
|
const dropdown = document.getElementById('issuerCA');
|
||||||
|
if (!dropdown) {
|
||||||
|
console.error('ERROR: Dropdown #issuerCA not found in DOM!');
|
||||||
|
// Vérifier si l'élément existe avec un autre ID
|
||||||
|
console.log('Available select elements:', document.querySelectorAll('select'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Dropdown found, populating...');
|
||||||
|
|
||||||
|
// Sauvegarder la sélection actuelle
|
||||||
|
const currentValue = dropdown.value;
|
||||||
|
|
||||||
|
// Vider et remplir
|
||||||
|
dropdown.innerHTML = '<option value="">-- Select Issuer --</option>';
|
||||||
|
|
||||||
|
allIssuers.forEach(issuer => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = issuer.id;
|
||||||
|
option.textContent = `${issuer.name} (${issuer.type})`;
|
||||||
|
dropdown.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restaurer la sélection si possible
|
||||||
|
if (currentValue && allIssuers.some(i => i.id === currentValue)) {
|
||||||
|
dropdown.value = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Dropdown populated with ${allIssuers.length} options`);
|
||||||
|
console.log('Dropdown HTML:', dropdown.innerHTML);
|
||||||
|
|
||||||
|
// 5. Afficher la modal
|
||||||
|
const modal = document.getElementById('createCertModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
console.log('Modal shown');
|
||||||
|
|
||||||
|
// Focus sur le premier champ
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstInput = modal.querySelector('input, select');
|
||||||
|
if (firstInput) firstInput.focus();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
console.error('ERROR: Modal #createCertModal not found!');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in showCreateCertModal:', error);
|
||||||
|
this.showError('Failed to open certificate form: ' + error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== showCreateCertModal END ===');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideModal() {
|
||||||
|
document.querySelectorAll('.modal').forEach(modal => {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateCA(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/cas`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: data.name,
|
||||||
|
common_name: data.common_name,
|
||||||
|
organization: data.organization,
|
||||||
|
country: data.country,
|
||||||
|
province: data.province || '',
|
||||||
|
locality: data.locality || '',
|
||||||
|
email: data.email || '',
|
||||||
|
key_size: parseInt(data.key_size) || 4096,
|
||||||
|
valid_years: parseInt(data.valid_years) || 10,
|
||||||
|
is_root: data.is_root === 'true'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess('CA created successfully');
|
||||||
|
this.hideModal();
|
||||||
|
e.target.reset();
|
||||||
|
await this.fetchAllData();
|
||||||
|
this.showTab(this.currentTab);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Failed to create CA');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('CA creation error:', error);
|
||||||
|
this.showError('Failed to create CA: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateSubCA(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/subcas`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: data.name,
|
||||||
|
common_name: data.common_name,
|
||||||
|
organization: data.organization,
|
||||||
|
email: data.email || '',
|
||||||
|
country: data.country,
|
||||||
|
province: data.province || '',
|
||||||
|
locality: data.locality || '',
|
||||||
|
parent_ca_id: data.parent_ca_id,
|
||||||
|
key_size: parseInt(data.key_size) || 4096,
|
||||||
|
valid_years: parseInt(data.valid_years) || 5
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess('Sub CA created successfully');
|
||||||
|
this.hideModal();
|
||||||
|
e.target.reset();
|
||||||
|
await this.fetchAllData();
|
||||||
|
this.showTab(this.currentTab);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Failed to create Sub CA');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SubCA creation error:', error);
|
||||||
|
this.showError('Failed to create Sub CA: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateCertificate(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dnsNames = data.dns_names ? data.dns_names.split(',').map(s => s.trim()).filter(s => s) : [];
|
||||||
|
const ipAddresses = data.ip_addresses ? data.ip_addresses.split(',').map(s => s.trim()).filter(s => s) : [];
|
||||||
|
|
||||||
|
const response = await fetch(`${this.apiBase}/certificates`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
common_name: data.common_name,
|
||||||
|
type: data.type,
|
||||||
|
dns_names: dnsNames,
|
||||||
|
ip_addresses: ipAddresses,
|
||||||
|
issuer_ca_id: data.issuer_ca_id,
|
||||||
|
key_size: parseInt(data.key_size) || 2048,
|
||||||
|
valid_days: parseInt(data.valid_days) || 365
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess('Certificate created successfully');
|
||||||
|
this.hideModal();
|
||||||
|
e.target.reset();
|
||||||
|
await this.fetchAllData();
|
||||||
|
this.showTab(this.currentTab);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Failed to create certificate');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Certificate creation error:', error);
|
||||||
|
this.showError('Failed to create certificate: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCA(id) {
|
||||||
|
if (!confirm('Delete this CA?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/cas/${id}`, { method: 'DELETE' });
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess('CA deleted');
|
||||||
|
await this.fetchAllData();
|
||||||
|
this.showTab(this.currentTab);
|
||||||
|
} else {
|
||||||
|
throw new Error('Delete failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Failed to delete CA');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSubCA(id) {
|
||||||
|
if (!confirm('Delete this Sub CA?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/subcas/${id}`, { method: 'DELETE' });
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess('Sub CA deleted');
|
||||||
|
await this.fetchAllData();
|
||||||
|
this.showTab(this.currentTab);
|
||||||
|
} else {
|
||||||
|
throw new Error('Delete failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Failed to delete Sub CA');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCertificate(id) {
|
||||||
|
if (!confirm('Delete this certificate?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/certificates/${id}`, { method: 'DELETE' });
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess('Certificate deleted');
|
||||||
|
await this.fetchAllData();
|
||||||
|
this.showTab(this.currentTab);
|
||||||
|
} else {
|
||||||
|
throw new Error('Delete failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Failed to delete certificate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeCertificate(id) {
|
||||||
|
const reason = prompt('Revocation reason:') || 'Administrative revocation';
|
||||||
|
if (!reason) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/certificates/${id}/revoke`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ reason })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess('Certificate revoked');
|
||||||
|
await this.fetchAllData();
|
||||||
|
this.showTab(this.currentTab);
|
||||||
|
} else {
|
||||||
|
throw new Error('Revocation failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Failed to revoke certificate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadCertificate(id) {
|
||||||
|
window.open(`${this.apiBase}/certificates/${id}/download/cert`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
async viewCA(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/cas/${id}`);
|
||||||
|
if (!response.ok) throw new Error('Not found');
|
||||||
|
|
||||||
|
const ca = await response.json();
|
||||||
|
this.showModal('CA Details', `
|
||||||
|
<h3>${ca.name}</h3>
|
||||||
|
<p><strong>Common Name:</strong> ${ca.common_name}</p>
|
||||||
|
<p><strong>Organization:</strong> ${ca.organization}</p>
|
||||||
|
<p><strong>Valid From:</strong> ${new Date(ca.valid_from).toLocaleString()}</p>
|
||||||
|
<p><strong>Valid To:</strong> ${new Date(ca.valid_to).toLocaleString()}</p>
|
||||||
|
<p><strong>Serial:</strong> ${ca.serial_number}</p>
|
||||||
|
<button class="btn" onclick="window.open('${this.apiBase}/cas/${id}/download/cert')">Download Certificate</button>
|
||||||
|
`);
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Failed to load CA details');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async viewSubCA(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/subcas/${id}`);
|
||||||
|
if (!response.ok) throw new Error('Not found');
|
||||||
|
|
||||||
|
const subca = await response.json();
|
||||||
|
this.showModal('Sub CA Details', `
|
||||||
|
<h3>${subca.name}</h3>
|
||||||
|
<p><strong>Common Name:</strong> ${subca.common_name}</p>
|
||||||
|
<p><strong>Organization:</strong> ${subca.organization}</p>
|
||||||
|
<p><strong>Valid From:</strong> ${new Date(subca.valid_from).toLocaleString()}</p>
|
||||||
|
<p><strong>Valid To:</strong> ${new Date(subca.valid_to).toLocaleString()}</p>
|
||||||
|
<p><strong>Serial:</strong> ${subca.serial_number}</p>
|
||||||
|
<button class="btn" onclick="window.open('${this.apiBase}/subcas/${id}/download/cert')">Download Certificate</button>
|
||||||
|
`);
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Failed to load Sub CA details');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async viewCertificate(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/certificates/${id}`);
|
||||||
|
if (!response.ok) throw new Error('Not found');
|
||||||
|
|
||||||
|
const cert = await response.json();
|
||||||
|
this.showModal('Certificate Details', `
|
||||||
|
<h3>${cert.common_name}</h3>
|
||||||
|
<p><strong>Type:</strong> ${cert.type}</p>
|
||||||
|
<p><strong>Valid From:</strong> ${new Date(cert.valid_from).toLocaleString()}</p>
|
||||||
|
<p><strong>Valid To:</strong> ${new Date(cert.valid_to).toLocaleString()}</p>
|
||||||
|
<p><strong>Status:</strong> ${cert.revoked ? 'Revoked' : new Date(cert.valid_to) > new Date() ? 'Valid' : 'Expired'}</p>
|
||||||
|
<p><strong>Serial:</strong> ${cert.serial_number}</p>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn" onclick="window.open('${this.apiBase}/certificates/${id}/download/cert')">Download Certificate</button>
|
||||||
|
<button class="btn" onclick="window.open('${this.apiBase}/certificates/${id}/download/key')">Download Private Key</button>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
} catch (error) {
|
||||||
|
this.showError('Failed to load certificate details');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(title, content) {
|
||||||
|
const modal = document.getElementById('viewModal');
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
modal.querySelector('h2').textContent = title;
|
||||||
|
modal.querySelector('.modal-body').innerHTML = content;
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess(message) {
|
||||||
|
this.showAlert(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
this.showAlert(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert(message, type) {
|
||||||
|
// Remove existing alerts
|
||||||
|
document.querySelectorAll('.alert').forEach(a => a.remove());
|
||||||
|
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = `alert alert-${type}`;
|
||||||
|
alert.textContent = message;
|
||||||
|
alert.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 10000;
|
||||||
|
animation: slideIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(alert);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
alert.style.animation = 'slideOut 0.3s ease';
|
||||||
|
setTimeout(() => alert.remove(), 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.pki = new PKIManager();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PKI Manager</title>
|
||||||
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1><i class="fas fa-shield-alt"></i> PKI Manager</h1>
|
||||||
|
<p>Manage your Certificate Authority infrastructure</p>
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<button class="btn" onclick="pki.debugData()" style="background: #38a169;">
|
||||||
|
<i class="fas fa-bug"></i> Debug Data
|
||||||
|
</button>
|
||||||
|
<button onclick="testDropdown()" style="padding: 8px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
Test Dropdown
|
||||||
|
</button>
|
||||||
|
<script>
|
||||||
|
function testDropdown() {
|
||||||
|
console.log('=== TEST DROPDOWN ===');
|
||||||
|
const dropdown = document.getElementById('issuerCA');
|
||||||
|
console.log('Dropdown:', dropdown);
|
||||||
|
console.log('Parent:', dropdown?.parentElement);
|
||||||
|
console.log('All selects:', document.querySelectorAll('select'));
|
||||||
|
|
||||||
|
// Simuler le remplissage
|
||||||
|
if (dropdown) {
|
||||||
|
dropdown.innerHTML = '';
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = 'test';
|
||||||
|
option.textContent = 'Test Option';
|
||||||
|
dropdown.appendChild(option);
|
||||||
|
console.log('Test option added');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<button class="nav-btn" data-tab="dashboard">
|
||||||
|
<i class="fas fa-home"></i> Dashboard
|
||||||
|
</button>
|
||||||
|
<button class="nav-btn" data-tab="cas">
|
||||||
|
<i class="fas fa-certificate"></i> CAs
|
||||||
|
</button>
|
||||||
|
<button class="nav-btn" data-tab="subcas">
|
||||||
|
<i class="fas fa-sitemap"></i> Sub CAs
|
||||||
|
</button>
|
||||||
|
<button class="nav-btn" data-tab="certificates">
|
||||||
|
<i class="fas fa-key"></i> Certificates
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Dashboard Tab -->
|
||||||
|
<div id="dashboardTab" class="tab-content">
|
||||||
|
<div id="dashboardContent">
|
||||||
|
<!-- Dashboard content will be loaded here -->
|
||||||
|
<div style="text-align: center; padding: 50px;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p>Loading dashboard...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CAs Tab -->
|
||||||
|
<div id="casTab" class="tab-content hidden">
|
||||||
|
<div id="casContent">
|
||||||
|
<div style="text-align: center; padding: 50px;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p>Loading CAs...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub CAs Tab -->
|
||||||
|
<div id="subcasTab" class="tab-content hidden">
|
||||||
|
<div id="subcasContent">
|
||||||
|
<div style="text-align: center; padding: 50px;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p>Loading Sub CAs...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Certificates Tab -->
|
||||||
|
<div id="certificatesTab" class="tab-content hidden">
|
||||||
|
<div id="certsContent">
|
||||||
|
<div style="text-align: center; padding: 50px;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p>Loading Certificates...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create CA Modal -->
|
||||||
|
<div id="createCAModal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button class="close-btn" onclick="pki.hideModal()" style="background: none; border: none; float: right; font-size: 24px; cursor: pointer;">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
<h2>Create Root CA</h2>
|
||||||
|
<form id="createCAForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caName">Name</label>
|
||||||
|
<input type="text" id="caName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caCommonName">Common Name</label>
|
||||||
|
<input type="text" id="caCommonName" name="common_name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caOrganization">Organization</label>
|
||||||
|
<input type="text" id="caOrganization" name="organization" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caCountry">Country (2 letters)</label>
|
||||||
|
<input type="text" id="caCountry" name="country" maxlength="2" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caProvince">Province/State</label>
|
||||||
|
<input type="text" id="caProvince" name="province">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caLocality">Locality/City</label>
|
||||||
|
<input type="text" id="caLocality" name="locality">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caEmail">Email</label>
|
||||||
|
<input type="email" id="caEmail" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caKeySize">Key Size</label>
|
||||||
|
<select id="caKeySize" name="key_size" required>
|
||||||
|
<option value="2048">2048 bits</option>
|
||||||
|
<option value="4096" selected>4096 bits</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="caValidYears">Validity (Years)</label>
|
||||||
|
<input type="number" id="caValidYears" name="valid_years" value="10" min="1" max="20" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="caIsRoot" name="is_root" value="true" checked>
|
||||||
|
Is Root CA
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Create CA</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Sub CA Modal -->
|
||||||
|
<div id="createSubCAModal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button class="close-btn" onclick="pki.hideModal()" style="background: none; border: none; float: right; font-size: 24px; cursor: pointer;">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
<h2>Create Sub CA</h2>
|
||||||
|
<form id="createSubCAForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaName">Name</label>
|
||||||
|
<input type="text" id="subcaName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaCommonName">Common Name</label>
|
||||||
|
<input type="text" id="subcaCommonName" name="common_name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaOrganization">Organization</label>
|
||||||
|
<input type="text" id="subcaOrganization" name="organization" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaEmail">Email (optional)</label>
|
||||||
|
<input type="email" id="subcaEmail" name="email" placeholder="Optional email address">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaCountry">Country (2 letters)</label>
|
||||||
|
<input type="text" id="subcaCountry" name="country" maxlength="2" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaProvince">Province/State</label>
|
||||||
|
<input type="text" id="subcaProvince" name="province">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaLocality">Locality/City</label>
|
||||||
|
<input type="text" id="subcaLocality" name="locality">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaParentCA">Parent CA</label>
|
||||||
|
<select id="parentCA" name="parent_ca_id" required></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaKeySize">Key Size</label>
|
||||||
|
<select id="subcaKeySize" name="key_size" required>
|
||||||
|
<option value="2048">2048 bits</option>
|
||||||
|
<option value="4096" selected>4096 bits</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subcaValidYears">Validity (Years)</label>
|
||||||
|
<input type="number" id="subcaValidYears" name="valid_years" value="5" min="1" max="10" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Create Sub CA</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Create Certificate Modal -->
|
||||||
|
|
||||||
|
<div id="createCertModal" class="modal hidden">
|
||||||
|
<div class="modal-content" style="max-width: 600px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||||
|
<h2 style="margin: 0;">Create Certificate</h2>
|
||||||
|
<button class="close-btn" onclick="pki.hideModal()"
|
||||||
|
style="background: none; border: none; font-size: 24px; cursor: pointer; color: #666;">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="createCertForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="certCommonName">Common Name *</label>
|
||||||
|
<input type="text" id="certCommonName" name="common_name" required
|
||||||
|
placeholder="e.g., server.example.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="certType">Certificate Type *</label>
|
||||||
|
<select id="certType" name="type" required>
|
||||||
|
<option value="">-- Select Type --</option>
|
||||||
|
<option value="server">Server Certificate</option>
|
||||||
|
<option value="client">Client Certificate</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IMPORTANT: Ce dropdown doit avoir l'ID "issuerCA" -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="issuerCA">Issuer (CA or Sub CA) *</label>
|
||||||
|
<select id="issuerCA" name="issuer_ca_id" required
|
||||||
|
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||||||
|
<option value="">-- Loading issuers... --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="certDNSNames">DNS Names (optional, comma-separated)</label>
|
||||||
|
<input type="text" id="certDNSNames" name="dns_names"
|
||||||
|
placeholder="e.g., example.com, www.example.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="certIPs">IP Addresses (optional, comma-separated)</label>
|
||||||
|
<input type="text" id="certIPs" name="ip_addresses"
|
||||||
|
placeholder="e.g., 192.168.1.1, 10.0.0.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="certKeySize">Key Size *</label>
|
||||||
|
<select id="certKeySize" name="key_size" required>
|
||||||
|
<option value="2048">2048 bits (Recommended)</option>
|
||||||
|
<option value="4096">4096 bits (High Security)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="certValidDays">Validity Period (Days) *</label>
|
||||||
|
<input type="number" id="certValidDays" name="valid_days"
|
||||||
|
value="365" min="1" max="3650" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; margin-top: 30px;">
|
||||||
|
<button type="submit" class="btn"
|
||||||
|
style="flex: 1; padding: 12px; background: #667eea; color: white;">
|
||||||
|
<i class="fas fa-plus"></i> Create Certificate
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn" onclick="pki.hideModal()"
|
||||||
|
style="padding: 12px; background: #ccc; color: #333;">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- View Details Modal -->
|
||||||
|
<div id="viewModal" class="modal hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button class="close-btn" onclick="pki.hideModal()" style="background: none; border: none; float: right; font-size: 24px; cursor: pointer;">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
<h2>View Details</h2>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Content will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
db = db.getSiblingDB('pki_db');
|
||||||
|
|
||||||
|
// Create collections
|
||||||
|
db.createCollection('cas');
|
||||||
|
db.createCollection('subcas');
|
||||||
|
db.createCollection('certificates');
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
db.cas.createIndex({ name: 1 }, { unique: true });
|
||||||
|
db.cas.createIndex({ common_name: 1 }, { unique: true });
|
||||||
|
|
||||||
|
db.subcas.createIndex({ name: 1 }, { unique: true });
|
||||||
|
db.subcas.createIndex({ common_name: 1 }, { unique: true });
|
||||||
|
db.subcas.createIndex({ parent_ca_id: 1 });
|
||||||
|
|
||||||
|
db.certificates.createIndex({ common_name: 1 });
|
||||||
|
db.certificates.createIndex({ issuer_ca_id: 1 });
|
||||||
|
db.certificates.createIndex({ serial_number: 1 }, { unique: true });
|
||||||
|
db.certificates.createIndex({ revoked: 1 });
|
||||||
|
|
||||||
|
print('PKI database initialized successfully');
|
||||||
Loading…
Reference in New Issue