mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e97b3951c | |||
| e54b2157c7 | |||
| 95dad52cea | |||
| 28dcae5865 | |||
| 4129c36f9e | |||
| 54cb1cf3da | |||
| a0569302c8 | |||
| 5a2f99196f | |||
| 91fbbf5dd9 | |||
| ca168928c7 | |||
| 4d2a9bd7b4 | |||
| 4c4be2ef41 | |||
| a22c615ac1 | |||
| 3ecf39814e | |||
| 770adbd3ff | |||
| 50119ac538 | |||
| b459245c5c | |||
| d4a0d5c68b | |||
| c4448594e2 | |||
| fb831208f4 | |||
| 054ad2ad20 | |||
| 171191c97d | |||
| 587cfabb4a | |||
| 3ede6461cf | |||
| 1dfd1f747e | |||
| 40665b0d8f | |||
| 94f4929749 | |||
| ad815b3412 | |||
| d41e16cab9 | |||
| 22e4728738 | |||
| 501b6f8440 | |||
| 3ea5bf6787 | |||
| 0a4e06614b | |||
| 45f41f87ff | |||
| 02defcb86a | |||
| 0c791898ff | |||
| ddcda59239 | |||
| 85dfc33191 | |||
| bb60e987e5 | |||
| d5e8487f44 | |||
| ab8c5d2ec4 | |||
| 452403d71e | |||
| 5d676d5993 | |||
| e239246d02 | |||
| 6c387b420c | |||
| ae84c9d8cc | |||
| 6bddbbf9d9 | |||
| d04b9278d2 | |||
| 1684d69fae | |||
| 3376f538fe | |||
| d6c9747c54 | |||
| e4646faf30 | |||
| fac5f382ec | |||
| 1053b779e4 | |||
| 9444ad56dc | |||
| 4d26e6b7b4 |
@@ -3,9 +3,13 @@ root = "."
|
|||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
cmd = "go build -o ./tmp/main ./cmd/api"
|
# Build binary utama
|
||||||
bin = "tmp/main"
|
cmd = "go build -o /lti-api/tmp/main ./cmd/api"
|
||||||
full_bin = "APP_ENV=dev ./tmp/main"
|
# Lokasi binary hasil build
|
||||||
|
bin = "/lti-api/tmp/main"
|
||||||
|
# Jalankan binary langsung dengan environment dev
|
||||||
|
full_bin = "APP_ENV=dev /lti-api/tmp/main"
|
||||||
|
# File yang dipantau oleh Air
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
exclude_dir = ["vendor", "tmp"]
|
exclude_dir = ["vendor", "tmp"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# server configuration
|
|
||||||
# Env value : prod || dev
|
|
||||||
VERSION=0.0.1
|
|
||||||
APP_ENV=dev
|
|
||||||
APP_HOST=0.0.0.0
|
|
||||||
APP_PORT=8081
|
|
||||||
APP_URL=http://localhost:8081
|
|
||||||
|
|
||||||
# database configuration
|
|
||||||
DB_HOST=postgresdb
|
|
||||||
DB_USER=postgres
|
|
||||||
DB_PASSWORD=changeme
|
|
||||||
DB_NAME=db_lti_erp
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_PORT_HOST=5542
|
|
||||||
|
|
||||||
# JWT
|
|
||||||
JWT_SECRET=changeme
|
|
||||||
JWT_ACCESS_EXP_MINUTES=30
|
|
||||||
JWT_REFRESH_EXP_DAYS=30
|
|
||||||
JWT_RESET_PASSWORD_EXP_MINUTES=10
|
|
||||||
JWT_VERIFY_EMAIL_EXP_MINUTES=10
|
|
||||||
|
|
||||||
# CORS
|
|
||||||
CORS_ALLOW_ORIGINS=changeme
|
|
||||||
CORS_ALLOW_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
|
||||||
CORS_ALLOW_HEADERS=Authorization,Content-Type,X-Requested-With
|
|
||||||
CORS_EXPOSE_HEADERS=Link,Location
|
|
||||||
CORS_ALLOW_CREDENTIALS=true
|
|
||||||
CORS_MAX_AGE=600
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_URL=redis://redis:6379/0
|
|
||||||
REDIS_PORT_HOST=6381
|
|
||||||
@@ -10,8 +10,13 @@ bin/
|
|||||||
*.exe
|
*.exe
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
|
Makefile
|
||||||
|
docker-compose.local.yml
|
||||||
|
docker-compose.yaml
|
||||||
|
Dockerfile.local
|
||||||
# Go build cache
|
# Go build cache
|
||||||
.gocache/
|
.gocache/
|
||||||
|
vendor
|
||||||
|
|
||||||
# Logs & reports
|
# Logs & reports
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
stages:
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
deploy-dev:
|
||||||
|
stage: deploy
|
||||||
|
image: alpine:3.20
|
||||||
|
variables:
|
||||||
|
DEPLOY_APP: "LTI-MBUGROUP"
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- echo "🧰 Installing dependencies..."
|
||||||
|
- apk update && apk add --no-cache openssh git curl
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||||
|
- chmod 600 ~/.ssh/id_rsa
|
||||||
|
- eval $(ssh-agent -s)
|
||||||
|
- ssh-add ~/.ssh/id_rsa
|
||||||
|
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
script:
|
||||||
|
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||||
|
- >
|
||||||
|
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||||
|
cd /home/devops/docker/deployment/development/lti-api &&
|
||||||
|
git fetch origin development &&
|
||||||
|
git reset --hard origin/development &&
|
||||||
|
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
||||||
|
"; then
|
||||||
|
STATUS='success';
|
||||||
|
else
|
||||||
|
STATUS='failed';
|
||||||
|
fi;
|
||||||
|
|
||||||
|
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
|
||||||
|
|
||||||
|
if [ "$STATUS" = "success" ]; then
|
||||||
|
COLOR=3066993;
|
||||||
|
TITLE="✅ Deployment API Succeeded";
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
|
||||||
|
else
|
||||||
|
COLOR=15158332;
|
||||||
|
TITLE="❌ Deployment API Failed Gaes";
|
||||||
|
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
|
||||||
|
fi;
|
||||||
|
|
||||||
|
echo "{
|
||||||
|
\"username\": \"CI Bot\",
|
||||||
|
\"embeds\": [{
|
||||||
|
\"title\": \"$TITLE\",
|
||||||
|
\"description\": \"$DESC\",
|
||||||
|
\"color\": $COLOR,
|
||||||
|
\"fields\": [
|
||||||
|
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
|
||||||
|
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
|
||||||
|
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
|
||||||
|
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}" > payload.json;
|
||||||
|
|
||||||
|
echo "📡 Sending notification to Discord...";
|
||||||
|
curl -sS -H "Content-Type: application/json" \
|
||||||
|
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||||
|
|
||||||
|
only:
|
||||||
|
- development
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: development
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
- cleanup
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🏗️ BUILD IMAGE (Overwrite :dev)
|
||||||
|
# ==============================
|
||||||
|
build_image:
|
||||||
|
stage: build
|
||||||
|
image: docker:latest
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
variables:
|
||||||
|
DOCKER_TLS_CERTDIR: "/certs"
|
||||||
|
script:
|
||||||
|
- echo "🔧 Building Docker image for :dev..."
|
||||||
|
- docker login -u gitlab-ci-token -p "$CI_JOB_TOKEN" "$CI_REGISTRY"
|
||||||
|
- docker build -f Dockerfile.local -t registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev .
|
||||||
|
- docker push registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev
|
||||||
|
only:
|
||||||
|
- development
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# 🚀 DEPLOY TO DEV SERVER
|
||||||
|
# ==============================
|
||||||
|
deploy_lti:
|
||||||
|
stage: deploy
|
||||||
|
image: alpine:latest
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache openssh-client bash curl
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||||
|
- chmod 600 ~/.ssh/id_rsa
|
||||||
|
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
script:
|
||||||
|
- echo "🚀 Deploy ke ${SERVER_USER}@${SERVER_IP} menggunakan image :dev"
|
||||||
|
- |
|
||||||
|
ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} bash -s <<REMOTE
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_NAME="lti-api"
|
||||||
|
DOCKER_IMAGE="registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev"
|
||||||
|
NETWORK_NAME="lti-network"
|
||||||
|
ENV_PATH="/home/devops/code/api/lti-api/.env.lti-api"
|
||||||
|
PORT=8081
|
||||||
|
|
||||||
|
echo "🔑 Login ke GitLab Registry..."
|
||||||
|
echo "${GITLAB_TOKEN}" | docker login -u "${GITLAB_USER}" --password-stdin registry.gitlab.com
|
||||||
|
|
||||||
|
echo "🛑 Stop & remove old container..."
|
||||||
|
docker stop "\${APP_NAME}" >/dev/null 2>&1 || true
|
||||||
|
docker rm -f "\${APP_NAME}" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo "🧹 Membersihkan container zombie di port \${PORT}..."
|
||||||
|
OLD_ID=\$(docker ps -aq --filter "publish=\${PORT}")
|
||||||
|
if [ -n "\${OLD_ID}" ]; then
|
||||||
|
echo "⚠️ Container lain masih pakai port \${PORT}, hapus..."
|
||||||
|
docker stop \${OLD_ID} >/dev/null 2>&1 || true
|
||||||
|
docker rm -f \${OLD_ID} >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🐳 Pull image baru..."
|
||||||
|
docker pull "\${DOCKER_IMAGE}"
|
||||||
|
|
||||||
|
echo "🚀 Run container baru..."
|
||||||
|
docker run -d --name "\${APP_NAME}" --restart always \
|
||||||
|
--env-file "\${ENV_PATH}" \
|
||||||
|
-p \${PORT}:8081 \
|
||||||
|
--network "\${NETWORK_NAME}" \
|
||||||
|
"\${DOCKER_IMAGE}"
|
||||||
|
|
||||||
|
echo "✅ Deployment selesai di port \${PORT}"
|
||||||
|
REMOTE
|
||||||
|
|
||||||
|
only:
|
||||||
|
- development
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# --- Load .env kalau ada, dan export ke shell child ---
|
|
||||||
ifneq (,$(wildcard .env))
|
|
||||||
include .env
|
|
||||||
export
|
|
||||||
endif
|
|
||||||
|
|
||||||
# --- Konfigurasi umum ---
|
|
||||||
COMPOSE ?= docker compose -f docker-compose.local.yml
|
|
||||||
NETWORK ?= lti-api_go-network
|
|
||||||
MIGRATE_IMAGE ?= migrate/migrate
|
|
||||||
MIGRATIONS_DIR := $(PWD)/internal/database/migrations
|
|
||||||
|
|
||||||
# Fallback agar tetap jalan meski .env kosong
|
|
||||||
DB_HOST ?= postgresdb
|
|
||||||
DB_PORT ?= 5432
|
|
||||||
DB_USER ?= postgres
|
|
||||||
DB_PASSWORD ?= postgres
|
|
||||||
DB_NAME ?= db_lti_erp
|
|
||||||
|
|
||||||
DB_URL := postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
|
||||||
|
|
||||||
# Tunggu DB ready memakai pg_isready dari image postgres
|
|
||||||
WAIT_DB := docker run --rm --network $(NETWORK) postgres:alpine \
|
|
||||||
sh -c 'until pg_isready -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d $(DB_NAME); do echo "waiting for postgres..."; sleep 1; done'
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
.DEFAULT_GOAL := start
|
|
||||||
|
|
||||||
# --- Daftar phony targets ---
|
|
||||||
.PHONY: start build test lint gen \
|
|
||||||
db-up wait-db \
|
|
||||||
migration-% migrate-up migrate-down migrate-fresh \
|
|
||||||
seed \
|
|
||||||
docker-local docker-down docker-nuke docker-cache psql
|
|
||||||
|
|
||||||
# --- Go workflow ---
|
|
||||||
start:
|
|
||||||
@go run cmd/api/main.go
|
|
||||||
|
|
||||||
build:
|
|
||||||
@go build -o tmp/app ./cmd/api
|
|
||||||
|
|
||||||
test:
|
|
||||||
@go test ./test/...
|
|
||||||
|
|
||||||
lint:
|
|
||||||
@golangci-lint run
|
|
||||||
|
|
||||||
# --- Compose / DB helpers ---
|
|
||||||
db-up:
|
|
||||||
@$(COMPOSE) up -d postgresdb
|
|
||||||
|
|
||||||
wait-db:
|
|
||||||
@$(WAIT_DB)
|
|
||||||
|
|
||||||
# --- Migration (pembuatan file) ---
|
|
||||||
# Contoh: make migration-create_users_table
|
|
||||||
# ":" akan diubah ke "_" (biar aman untuk nama file)
|
|
||||||
migration-%:
|
|
||||||
@migrate create -ext sql -dir $(MIGRATIONS_DIR) $(subst :,_,$*)
|
|
||||||
|
|
||||||
# --- Migration (apply via docker image 'migrate') ---
|
|
||||||
migrate-up: db-up wait-db
|
|
||||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" up
|
|
||||||
|
|
||||||
# Contoh:
|
|
||||||
# make migrate-down step=2 → rollback 2 step
|
|
||||||
# make migrate-down → rollback semua
|
|
||||||
|
|
||||||
migrate-down: db-up wait-db
|
|
||||||
@if [ -n "$(step)" ]; then \
|
|
||||||
echo "⬇️ Migrating down $(step) step(s)..."; \
|
|
||||||
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down $(step); \
|
|
||||||
else \
|
|
||||||
echo "⬇️ Migrating down ALL steps..."; \
|
|
||||||
docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" down -all; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
migrate-fresh: migrate-down migrate-up
|
|
||||||
@true
|
|
||||||
|
|
||||||
# Pakai: make migrate-force v=20250917120000
|
|
||||||
migrate-force:
|
|
||||||
@docker run --rm -v $(MIGRATIONS_DIR):/migrations --network $(NETWORK) \
|
|
||||||
$(MIGRATE_IMAGE) -path=/migrations/ -database "$(DB_URL)" force $(v)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Seeder ---
|
|
||||||
seed: db-up wait-db
|
|
||||||
@$(COMPOSE) run --rm app go run cmd/seed/main.go
|
|
||||||
|
|
||||||
# --- Docker orchestration convenience ---
|
|
||||||
docker-local:
|
|
||||||
@$(COMPOSE) up --build -d
|
|
||||||
|
|
||||||
docker-down:
|
|
||||||
@$(COMPOSE) down --remove-orphans
|
|
||||||
|
|
||||||
# ⚠️ Akan menghapus container, images dan volumes.
|
|
||||||
docker-nuke:
|
|
||||||
@$(COMPOSE) down --rmi all --volumes --remove-orphans
|
|
||||||
|
|
||||||
docker-cache:
|
|
||||||
@docker builder prune -f
|
|
||||||
|
|
||||||
# --- PSQL shell ke DB di container ---
|
|
||||||
psql: db-up
|
|
||||||
@$(COMPOSE) exec -it postgresdb psql -U $(DB_USER) -d $(DB_NAME)
|
|
||||||
|
|
||||||
# Single feature
|
|
||||||
# example: make gen feat=product-category
|
|
||||||
|
|
||||||
# Sub feature
|
|
||||||
# make gen feat=master/area
|
|
||||||
gen:
|
|
||||||
@go run tools/gen.go $(feat)
|
|
||||||
# @goimports -w internal
|
|
||||||
@@ -9,10 +9,13 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/cache"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
"gitlab.com/mbugroup/lti-api.git/internal/database"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/route"
|
"gitlab.com/mbugroup/lti-api.git/internal/route"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -33,6 +36,7 @@ func main() {
|
|||||||
defer closeDatabase(db)
|
defer closeDatabase(db)
|
||||||
rdb := setupRedis()
|
rdb := setupRedis()
|
||||||
defer rdb.Close()
|
defer rdb.Close()
|
||||||
|
setupSSO(ctx, rdb)
|
||||||
setupRoutes(app, db, rdb)
|
setupRoutes(app, db, rdb)
|
||||||
|
|
||||||
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
|
address := fmt.Sprintf("%s:%d", config.AppHost, config.AppPort)
|
||||||
@@ -52,10 +56,47 @@ func setupRedis() *redis.Client {
|
|||||||
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
if err := rdb.Ping(context.Background()).Err(); err != nil {
|
||||||
utils.Log.Fatalf("Redis ping failed: %v", err)
|
utils.Log.Fatalf("Redis ping failed: %v", err)
|
||||||
}
|
}
|
||||||
|
cache.SetRedis(rdb)
|
||||||
utils.Log.Infof("Redis connected: %s", config.RedisURL)
|
utils.Log.Infof("Redis connected: %s", config.RedisURL)
|
||||||
return rdb
|
return rdb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupSSO(ctx context.Context, rdb *redis.Client) {
|
||||||
|
const (
|
||||||
|
maxAttempts = 12
|
||||||
|
retryDelay = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||||
|
if err := sso.Init(ctx, config.SSOJWKSURL, config.SSOIssuer, config.SSOAllowedAudiences); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
utils.Log.WithError(err).Warnf("SSO initialization attempt %d/%d failed", attempt, maxAttempts)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
utils.Log.Fatalf("SSO initialization aborted: %v", ctx.Err())
|
||||||
|
case <-time.After(retryDelay):
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lastErr = nil
|
||||||
|
if attempt > 1 {
|
||||||
|
utils.Log.Infof("SSO initialization succeeded after %d attempts", attempt)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil {
|
||||||
|
utils.Log.Fatalf("SSO initialization failed: %v", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rdb != nil {
|
||||||
|
session.SetRevocationStore(session.NewRevocationStore(rdb, config.SSOTokenBlacklistPrefix))
|
||||||
|
} else {
|
||||||
|
session.SetRevocationStore(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setupFiberApp() *fiber.App {
|
func setupFiberApp() *fiber.App {
|
||||||
app := fiber.New(config.FiberConfig())
|
app := fiber.New(config.FiberConfig())
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=Postgres@Secure2025!
|
||||||
|
POSTGRES_DB=db_lti_erp
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 🧩 INIT SCRIPT: CREATE LIMITED APP USER FOR LTI API
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Buat user aplikasi jika belum ada
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_lti_user') THEN
|
||||||
|
CREATE ROLE app_lti_user WITH LOGIN PASSWORD 'AppLti@Secure2025!' NOINHERIT NOCREATEROLE NOCREATEDB NOSUPERUSER;
|
||||||
|
RAISE NOTICE '✅ Role app_lti_user created successfully.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'ℹ️ Role app_lti_user already exists.';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Buat database jika belum ada
|
||||||
|
DO
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'db_lti_erp') THEN
|
||||||
|
CREATE DATABASE db_lti_erp OWNER app_lti_user;
|
||||||
|
RAISE NOTICE '✅ Database db_lti_erp created and owned by app_lti_user.';
|
||||||
|
ELSE
|
||||||
|
RAISE NOTICE 'ℹ️ Database db_lti_erp already exists.';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
\connect db_lti_erp
|
||||||
|
|
||||||
|
-- Beri hak CRUD untuk app_lti_user
|
||||||
|
GRANT CONNECT ON DATABASE db_lti_erp TO app_lti_user;
|
||||||
|
GRANT USAGE ON SCHEMA public TO app_lti_user;
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_lti_user;
|
||||||
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_lti_user;
|
||||||
|
|
||||||
|
-- Set default privileges agar tabel baru juga bisa diakses
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_lti_user;
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
||||||
|
GRANT USAGE, SELECT ON SEQUENCES TO app_lti_user;
|
||||||
|
|
||||||
|
-- Tampilkan hasil
|
||||||
|
\du app_lti_user
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
services:
|
|
||||||
postgresdb:
|
|
||||||
image: postgres:alpine
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "${DB_PORT_HOST:-5542}:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${DB_USER:-postgres}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
|
||||||
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
|
||||||
volumes:
|
|
||||||
- dbdata:/var/lib/postgresql/data
|
|
||||||
- ./internal/database/init:/docker-entrypoint-initdb.d
|
|
||||||
networks: [go-network]
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
|
|
||||||
]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "${REDIS_PORT_HOST:-6381}:6379"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
networks: [go-network]
|
|
||||||
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.local
|
|
||||||
image: cosmtrek/air:v1.52.3
|
|
||||||
working_dir: /lti-api
|
|
||||||
volumes:
|
|
||||||
- .:/lti-api
|
|
||||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
|
||||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
|
||||||
command: air -c .air.toml
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
DB_HOST: postgresdb
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_USER: ${DB_USER:-postgres}
|
|
||||||
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
|
||||||
DB_NAME: ${DB_NAME:-db_lti_erp}
|
|
||||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
|
||||||
ports:
|
|
||||||
- "${APP_PORT:-8081}:8081"
|
|
||||||
depends_on:
|
|
||||||
postgresdb:
|
|
||||||
condition: service_healthy
|
|
||||||
networks: [go-network]
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
dbdata:
|
|
||||||
go-mod-cache:
|
|
||||||
go-build-cache:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
go-network:
|
|
||||||
name: lti-api_go-network
|
|
||||||
driver: bridge
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
services:
|
||||||
|
dev-api-lti:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: dev-api-lti
|
||||||
|
working_dir: /lti-api
|
||||||
|
command: ["/bin/sh", "scripts/entrypoint.sh"]
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
# override agar koneksi ke container internal
|
||||||
|
DB_HOST: dev-postgres-lti
|
||||||
|
DB_PORT: 5432
|
||||||
|
REDIS_URL: redis://dev-redis-lti:6379/0
|
||||||
|
volumes:
|
||||||
|
- .:/lti-api
|
||||||
|
- ./.air.toml:/lti-api/.air.toml:ro
|
||||||
|
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
||||||
|
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
||||||
|
depends_on:
|
||||||
|
- dev-postgres-lti
|
||||||
|
- dev-redis-lti
|
||||||
|
networks:
|
||||||
|
- lti-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "2.0"
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
dev-postgres-lti:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: dev-postgres-lti
|
||||||
|
restart: always
|
||||||
|
env_file:
|
||||||
|
- credential/.env.db
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- dev-postgres-lti-data:/var/lib/postgresql/data
|
||||||
|
- ./credential:/docker-entrypoint-initdb.d:ro
|
||||||
|
networks:
|
||||||
|
- lti-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 5s
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
dev-redis-lti:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: dev-redis-lti
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6380:6379"
|
||||||
|
networks:
|
||||||
|
- lti-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: "0.2"
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
networks:
|
||||||
|
lti-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dev-postgres-lti-data:
|
||||||
@@ -3,12 +3,14 @@ module gitlab.com/mbugroup/lti-api.git
|
|||||||
go 1.23
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/MicahParks/keyfunc/v2 v2.1.0
|
||||||
github.com/bytedance/sonic v1.12.1
|
github.com/bytedance/sonic v1.12.1
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-playground/validator/v10 v10.27.0
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/gofiber/contrib/jwt v1.0.10
|
github.com/gofiber/contrib/jwt v1.0.10
|
||||||
github.com/gofiber/fiber/v2 v2.52.5
|
github.com/gofiber/fiber/v2 v2.52.5
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/jackc/pgconn v1.14.1
|
||||||
github.com/redis/go-redis/v9 v9.14.0
|
github.com/redis/go-redis/v9 v9.14.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
@@ -18,7 +20,6 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
@@ -34,7 +35,10 @@ require (
|
|||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/
|
|||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
@@ -43,6 +47,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gofiber/contrib/jwt v1.0.10 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
|
github.com/gofiber/contrib/jwt v1.0.10 h1:/ilGepl6i0Bntl0Zcd+lAzagY8BiS1+fEiAj32HMApk=
|
||||||
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU=
|
github.com/gofiber/contrib/jwt v1.0.10/go.mod h1:1qBENE6sZ6PPT4xIpBzx1VxeyROQO7sj48OlM1I9qdU=
|
||||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
||||||
@@ -57,12 +62,47 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||||
|
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||||
|
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||||
|
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||||
|
github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4=
|
||||||
|
github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
|
||||||
|
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
|
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||||
|
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
|
||||||
|
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||||
|
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||||
|
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||||
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
|
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -75,16 +115,28 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
|||||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
@@ -96,6 +148,7 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
@@ -109,10 +162,17 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||||
|
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||||
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
@@ -126,10 +186,15 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
|||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -150,24 +215,41 @@ github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRV
|
|||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -175,7 +257,14 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -184,28 +273,43 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.4/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
Vendored
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
redisClient *redis.Client
|
||||||
|
mu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetRedis assigns the global redis client used across the application.
|
||||||
|
func SetRedis(client *redis.Client) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
redisClient = client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis returns the configured redis client. It may be nil if not yet initialised.
|
||||||
|
func Redis() *redis.Client {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
return redisClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustRedis returns the redis client or panics if it has not been set.
|
||||||
|
func MustRedis() *redis.Client {
|
||||||
|
mu.RLock()
|
||||||
|
client := redisClient
|
||||||
|
mu.RUnlock()
|
||||||
|
if client == nil {
|
||||||
|
panic(errors.New("redis client not initialised"))
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package capabilities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
||||||
|
// to toggle features. Only permissions recognized by the application are exposed.
|
||||||
|
func FromPermissions(perms []string) map[string]bool {
|
||||||
|
if len(perms) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]bool)
|
||||||
|
for _, perm := range perms {
|
||||||
|
if key, ok := normalizeAndAllow(perm); ok {
|
||||||
|
out[key] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAndAllow(perm string) (string, bool) {
|
||||||
|
perm = strings.ToLower(strings.TrimSpace(perm))
|
||||||
|
if perm == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if _, ok := allowed[perm]; !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return perm, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowed = map[string]struct{}{
|
||||||
|
recordings.PermissionRecordingRead: {},
|
||||||
|
recordings.PermissionRecordingCreate: {},
|
||||||
|
recordings.PermissionRecordingUpdate: {},
|
||||||
|
recordings.PermissionRecordingDelete: {},
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ type BaseRepository[T any] interface {
|
|||||||
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
|
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
|
||||||
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
|
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
|
||||||
GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
|
GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
|
||||||
|
First(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) (*T, error)
|
||||||
|
|
||||||
CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
|
CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
|
CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
@@ -96,6 +97,21 @@ func (r *BaseRepositoryImpl[T]) GetByIDs(
|
|||||||
return entities, nil
|
return entities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *BaseRepositoryImpl[T]) First(
|
||||||
|
ctx context.Context,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) (*T, error) {
|
||||||
|
entity := new(T)
|
||||||
|
q := r.db.WithContext(ctx)
|
||||||
|
if modifier != nil {
|
||||||
|
q = modifier(q)
|
||||||
|
}
|
||||||
|
if err := q.First(entity).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---- CREATE ----
|
// ---- CREATE ----
|
||||||
func (r *BaseRepositoryImpl[T]) CreateOne(
|
func (r *BaseRepositoryImpl[T]) CreateOne(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|||||||
+164
-22
@@ -2,36 +2,69 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SSOClientConfig struct {
|
||||||
|
PublicID string `json:"public_id"`
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
// Prompt string `json:"prompt"`
|
||||||
|
DefaultReturnURI string `json:"default_return_uri"`
|
||||||
|
AllowedReturnOrigins []string `json:"allowed_return_origins"`
|
||||||
|
SyncSecret string `json:"sync_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
IsProd bool
|
IsProd bool
|
||||||
AppHost string
|
AppHost string
|
||||||
Version string
|
Version string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
AppPort int
|
AppPort int
|
||||||
DBHost string
|
DBHost string
|
||||||
DBUser string
|
DBUser string
|
||||||
DBPassword string
|
DBPassword string
|
||||||
DBName string
|
DBName string
|
||||||
DBPort int
|
DBPort int
|
||||||
JWTSecret string
|
DBSSLMode string
|
||||||
JWTAccessExp int
|
DBSSLRootCert string
|
||||||
JWTRefreshExp int
|
DBSSLCert string
|
||||||
JWTResetPasswordExp int
|
DBSSLKey string
|
||||||
JWTVerifyEmailExp int
|
JWTSecret string
|
||||||
RedisURL string
|
JWTAccessExp int
|
||||||
CORSAllowOrigins []string
|
JWTRefreshExp int
|
||||||
CORSAllowMethods []string
|
JWTResetPasswordExp int
|
||||||
CORSAllowHeaders []string
|
JWTVerifyEmailExp int
|
||||||
CORSExposeHeaders []string
|
RedisURL string
|
||||||
CORSAllowCredentials bool
|
CORSAllowOrigins []string
|
||||||
CORSMaxAge int
|
CORSAllowMethods []string
|
||||||
|
CORSAllowHeaders []string
|
||||||
|
CORSExposeHeaders []string
|
||||||
|
CORSAllowCredentials bool
|
||||||
|
CORSMaxAge int
|
||||||
|
SSOIssuer string
|
||||||
|
SSOJWKSURL string
|
||||||
|
SSOAllowedAudiences []string
|
||||||
|
SSOAuthorizeURL string
|
||||||
|
SSOTokenURL string
|
||||||
|
SSOGetMeURL string
|
||||||
|
SSOClients map[string]SSOClientConfig
|
||||||
|
SSOAccessCookieName string
|
||||||
|
SSORefreshCookieName string
|
||||||
|
SSOCookieDomain string
|
||||||
|
SSOCookieSecure bool
|
||||||
|
SSOCookieSameSite string
|
||||||
|
SSOTokenBlacklistPrefix string
|
||||||
|
SSOPKCETTL time.Duration
|
||||||
|
SSOUserSyncDrift time.Duration
|
||||||
|
SSOUserSyncNonceTTL time.Duration
|
||||||
|
SSOUserSyncMaxBodyBytes int
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -50,6 +83,10 @@ func init() {
|
|||||||
DBPassword = viper.GetString("DB_PASSWORD")
|
DBPassword = viper.GetString("DB_PASSWORD")
|
||||||
DBName = viper.GetString("DB_NAME")
|
DBName = viper.GetString("DB_NAME")
|
||||||
DBPort = viper.GetInt("DB_PORT")
|
DBPort = viper.GetInt("DB_PORT")
|
||||||
|
DBSSLMode = defaultString(viper.GetString("DB_SSLMODE"), "disable")
|
||||||
|
DBSSLRootCert = strings.TrimSpace(viper.GetString("DB_SSLROOTCERT"))
|
||||||
|
DBSSLCert = strings.TrimSpace(viper.GetString("DB_SSLCERT"))
|
||||||
|
DBSSLKey = strings.TrimSpace(viper.GetString("DB_SSLKEY"))
|
||||||
|
|
||||||
// jwt configuration
|
// jwt configuration
|
||||||
JWTSecret = viper.GetString("JWT_SECRET")
|
JWTSecret = viper.GetString("JWT_SECRET")
|
||||||
@@ -68,6 +105,44 @@ func init() {
|
|||||||
|
|
||||||
// Redis
|
// Redis
|
||||||
RedisURL = viper.GetString("REDIS_URL")
|
RedisURL = viper.GetString("REDIS_URL")
|
||||||
|
|
||||||
|
// SSO integration
|
||||||
|
SSOIssuer = viper.GetString("SSO_ISSUER")
|
||||||
|
SSOJWKSURL = viper.GetString("SSO_JWKS_URL")
|
||||||
|
SSOAllowedAudiences = parseList("SSO_ALLOWED_AUDIENCES")
|
||||||
|
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
|
||||||
|
SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
|
||||||
|
SSOGetMeURL = viper.GetString("SSO_GETME_URL")
|
||||||
|
SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access")
|
||||||
|
SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh")
|
||||||
|
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
|
||||||
|
SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE")
|
||||||
|
SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax")
|
||||||
|
SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist")
|
||||||
|
if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 {
|
||||||
|
SSOPKCETTL = time.Duration(ttl) * time.Second
|
||||||
|
} else {
|
||||||
|
SSOPKCETTL = 5 * time.Minute
|
||||||
|
}
|
||||||
|
SSOClients = loadSSOClients("SSO_CLIENTS")
|
||||||
|
if drift := viper.GetInt("SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS"); drift > 0 {
|
||||||
|
SSOUserSyncDrift = time.Duration(drift) * time.Second
|
||||||
|
} else {
|
||||||
|
SSOUserSyncDrift = 2 * time.Minute
|
||||||
|
}
|
||||||
|
if ttl := viper.GetInt("SSO_USER_SYNC_NONCE_TTL_SECONDS"); ttl > 0 {
|
||||||
|
SSOUserSyncNonceTTL = time.Duration(ttl) * time.Second
|
||||||
|
} else {
|
||||||
|
SSOUserSyncNonceTTL = 10 * time.Minute
|
||||||
|
}
|
||||||
|
SSOUserSyncMaxBodyBytes = viper.GetInt("SSO_USER_SYNC_MAX_BODY_BYTES")
|
||||||
|
if SSOUserSyncMaxBodyBytes <= 0 {
|
||||||
|
SSOUserSyncMaxBodyBytes = 32 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsProd {
|
||||||
|
ensureProdConfig()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() {
|
func loadConfig() {
|
||||||
@@ -117,3 +192,70 @@ func parseListWithDefault(key, def string) []string {
|
|||||||
}
|
}
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadSSOClients(key string) map[string]SSOClientConfig {
|
||||||
|
clients := make(map[string]SSOClientConfig)
|
||||||
|
raw := strings.TrimSpace(viper.GetString(key))
|
||||||
|
if raw == "" {
|
||||||
|
return clients
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(raw), &clients); err != nil {
|
||||||
|
utils.Log.Errorf("Failed to parse %s: %v", key, err)
|
||||||
|
return make(map[string]SSOClientConfig)
|
||||||
|
}
|
||||||
|
result := make(map[string]SSOClientConfig, len(clients))
|
||||||
|
for alias, cfg := range clients {
|
||||||
|
alias = strings.ToLower(strings.TrimSpace(alias))
|
||||||
|
for i, origin := range cfg.AllowedReturnOrigins {
|
||||||
|
cfg.AllowedReturnOrigins[i] = strings.TrimSpace(origin)
|
||||||
|
}
|
||||||
|
cfg.SyncSecret = strings.TrimSpace(cfg.SyncSecret)
|
||||||
|
result[alias] = cfg
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultString(v, def string) string {
|
||||||
|
if strings.TrimSpace(v) == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureProdConfig() {
|
||||||
|
if SSOAuthorizeURL == "" || !strings.HasPrefix(SSOAuthorizeURL, "https://") {
|
||||||
|
panic("SSO_AUTHORIZE_URL must be https in production")
|
||||||
|
}
|
||||||
|
if SSOTokenURL == "" || !strings.HasPrefix(SSOTokenURL, "https://") {
|
||||||
|
panic("SSO_TOKEN_URL must be https in production")
|
||||||
|
}
|
||||||
|
if SSOGetMeURL == "" || !strings.HasPrefix(SSOGetMeURL, "https://") {
|
||||||
|
panic("SSO_GETME_URL must be https in production")
|
||||||
|
}
|
||||||
|
if !SSOCookieSecure {
|
||||||
|
panic("SSO_COOKIE_SECURE must be true in production")
|
||||||
|
}
|
||||||
|
if SSOCookieDomain == "" {
|
||||||
|
panic("SSO_COOKIE_DOMAIN must be configured in production")
|
||||||
|
}
|
||||||
|
if len(SSOAllowedAudiences) == 0 {
|
||||||
|
panic("SSO_ALLOWED_AUDIENCES must contain at least one audience in production")
|
||||||
|
}
|
||||||
|
for alias, cfg := range SSOClients {
|
||||||
|
if strings.TrimSpace(cfg.SyncSecret) == "" {
|
||||||
|
panic(fmt.Sprintf("SSO_CLIENTS[%s].sync_secret must be configured in production", alias))
|
||||||
|
}
|
||||||
|
if len(cfg.SyncSecret) < 16 {
|
||||||
|
panic(fmt.Sprintf("SSO_CLIENTS[%s].sync_secret must be at least 16 characters", alias))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if SSOUserSyncDrift <= 0 {
|
||||||
|
panic("SSO_USER_SYNC_SIGNATURE_DRIFT_SECONDS must be greater than zero in production")
|
||||||
|
}
|
||||||
|
if SSOUserSyncNonceTTL <= 0 {
|
||||||
|
panic("SSO_USER_SYNC_NONCE_TTL_SECONDS must be greater than zero in production")
|
||||||
|
}
|
||||||
|
if SSOUserSyncMaxBodyBytes <= 0 {
|
||||||
|
panic("SSO_USER_SYNC_MAX_BODY_BYTES must be greater than zero in production")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
@@ -13,10 +14,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Connect(dbHost, dbName string) *gorm.DB {
|
func Connect(dbHost, dbName string) *gorm.DB {
|
||||||
dsn := fmt.Sprintf(
|
parts := []string{
|
||||||
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
|
fmt.Sprintf("host=%s", dbHost),
|
||||||
dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort,
|
fmt.Sprintf("user=%s", config.DBUser),
|
||||||
)
|
fmt.Sprintf("password=%s", config.DBPassword),
|
||||||
|
fmt.Sprintf("dbname=%s", dbName),
|
||||||
|
fmt.Sprintf("port=%d", config.DBPort),
|
||||||
|
fmt.Sprintf("sslmode=%s", config.DBSSLMode),
|
||||||
|
"TimeZone=Asia/Shanghai",
|
||||||
|
}
|
||||||
|
if config.DBSSLRootCert != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("sslrootcert=%s", config.DBSSLRootCert))
|
||||||
|
}
|
||||||
|
if config.DBSSLCert != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("sslcert=%s", config.DBSSLCert))
|
||||||
|
}
|
||||||
|
if config.DBSSLKey != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("sslkey=%s", config.DBSSLKey))
|
||||||
|
}
|
||||||
|
dsn := strings.Join(parts, " ")
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Info),
|
Logger: logger.Default.LogMode(logger.Info),
|
||||||
|
|||||||
@@ -9,13 +9,9 @@ CREATE TABLE users (
|
|||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user)
|
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
|
||||||
WHERE
|
|
||||||
deleted_at IS NULL;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_email_unique ON users (email)
|
CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;
|
||||||
WHERE
|
|
||||||
deleted_at IS NULL;
|
|
||||||
|
|
||||||
-- FLAGS
|
-- FLAGS
|
||||||
CREATE TABLE flags (
|
CREATE TABLE flags (
|
||||||
@@ -334,4 +330,4 @@ CREATE INDEX stock_logs_created_by_idx ON stock_logs (created_by);
|
|||||||
|
|
||||||
CREATE INDEX stock_logs_created_at_idx ON stock_logs (created_at);
|
CREATE INDEX stock_logs_created_at_idx ON stock_logs (created_at);
|
||||||
|
|
||||||
CREATE INDEX stock_logs_deleted_at_idx ON stock_logs (deleted_at);
|
CREATE INDEX stock_logs_deleted_at_idx ON stock_logs (deleted_at);
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
DROP CONSTRAINT IF EXISTS users_id_user_key;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD CONSTRAINT users_id_user_key UNIQUE (id_user);
|
||||||
+177
-85
@@ -1,101 +1,193 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
// import (
|
import (
|
||||||
// "strings"
|
"strings"
|
||||||
|
|
||||||
// "gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
// service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
// "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
// "github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
// )
|
)
|
||||||
|
|
||||||
// func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
|
const (
|
||||||
// return func(c *fiber.Ctx) error {
|
authContextLocalsKey = "auth.context"
|
||||||
// authHeader := c.Get("Authorization")
|
authUserLocalsKey = "auth.user"
|
||||||
// token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
)
|
||||||
|
|
||||||
// if token == "" {
|
// AuthContext keeps authentication details captured by the middleware.
|
||||||
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
type AuthContext struct {
|
||||||
// }
|
Token string
|
||||||
|
Verification *sso.VerificationResult
|
||||||
|
User *entity.User
|
||||||
|
Roles []sso.Role
|
||||||
|
Permissions map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
// userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)
|
// Auth validates the incoming request against the central SSO access token and
|
||||||
// if err != nil {
|
// loads the corresponding local user. Optional scopes can be provided to enforce
|
||||||
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
// fine-grained authorization using the SSO access token scopes.
|
||||||
// }
|
func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
token := bearerToken(c)
|
||||||
|
if token == "" {
|
||||||
|
token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
// // Only end-user subjects are allowed by this middleware. Service tokens
|
verification, err := sso.VerifyAccessToken(token)
|
||||||
// if verification.UserID == 0 {
|
if err != nil {
|
||||||
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
utils.Log.WithError(err).Warn("auth: token verification failed")
|
||||||
// }
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
// // Fail-closed on revocation check errors for stricter security posture.
|
if verification.UserID == 0 {
|
||||||
// if revoker := session.GetRevocationStore(); revoker != nil {
|
return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
|
||||||
// if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
|
}
|
||||||
// revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
|
|
||||||
// if err != nil {
|
|
||||||
// utils.Log.WithError(err).Warn("failed to check token revocation")
|
|
||||||
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
|
||||||
// }
|
|
||||||
// if revoked {
|
|
||||||
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// user, err := userService.GetBySSOUserID(c, verification.UserID)
|
if err := ensureNotRevoked(c, token, verification); err != nil {
|
||||||
// if err != nil || user == nil {
|
return err
|
||||||
// return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// if len(requiredRights) > 0 && verification.Claims != nil {
|
user, err := userService.GetBySSOUserID(c, verification.UserID)
|
||||||
// if !hasAllScopes(verification.Claims.Scopes(), requiredRights) {
|
if err != nil || user == nil {
|
||||||
// return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
|
utils.Log.WithError(err).Warn("auth: failed to resolve user from repository")
|
||||||
// }
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
// }
|
}
|
||||||
|
|
||||||
// c.Locals("user", user)
|
if len(requiredScopes) > 0 {
|
||||||
|
if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// // if len(requiredRights) > 0 {
|
var roles []sso.Role
|
||||||
// // userRights, hasRights := config.RoleRights[user.Role]
|
permissions := make(map[string]struct{})
|
||||||
// // if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
|
if verification.UserID != 0 {
|
||||||
// // return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
|
if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
|
||||||
// // }
|
utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
|
||||||
// // }
|
} else if profile != nil {
|
||||||
|
roles = profile.Roles
|
||||||
|
for _, perm := range profile.PermissionNames() {
|
||||||
|
if perm != "" {
|
||||||
|
permissions[perm] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// return c.Next()
|
ctx := &AuthContext{
|
||||||
// }
|
Token: token,
|
||||||
// }
|
Verification: verification,
|
||||||
|
User: user,
|
||||||
|
Roles: roles,
|
||||||
|
Permissions: permissions,
|
||||||
|
}
|
||||||
|
|
||||||
// // bearerToken extracts a Bearer token from the Authorization header using
|
c.Locals(authContextLocalsKey, ctx)
|
||||||
// // case-insensitive scheme matching and tolerant whitespace handling.
|
c.Locals(authUserLocalsKey, user)
|
||||||
// func bearerToken(c *fiber.Ctx) string {
|
|
||||||
// parts := strings.Fields(c.Get("Authorization"))
|
|
||||||
// if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
|
|
||||||
// return strings.TrimSpace(parts[1])
|
|
||||||
// }
|
|
||||||
// return ""
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func hasAllScopes(have, required []string) bool {
|
return c.Next()
|
||||||
// if len(required) == 0 {
|
}
|
||||||
// return true
|
}
|
||||||
// }
|
|
||||||
// set := make(map[string]struct{}, len(have))
|
// AuthenticatedUser returns the authenticated user populated by Auth.
|
||||||
// for _, s := range have {
|
func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
|
||||||
// s = strings.ToLower(strings.TrimSpace(s))
|
value := c.Locals(authUserLocalsKey)
|
||||||
// if s != "" {
|
if user, ok := value.(*entity.User); ok && user != nil {
|
||||||
// set[s] = struct{}{}
|
return user, true
|
||||||
// }
|
}
|
||||||
// }
|
return nil, false
|
||||||
// for _, r := range required {
|
}
|
||||||
// r = strings.ToLower(strings.TrimSpace(r))
|
|
||||||
// if r == "" {
|
// AuthDetails returns the full authentication context (token, claims, user).
|
||||||
// continue
|
func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
|
||||||
// }
|
value := c.Locals(authContextLocalsKey)
|
||||||
// if _, ok := set[r]; !ok {
|
if ctx, ok := value.(*AuthContext); ok && ctx != nil {
|
||||||
// return false
|
return ctx, true
|
||||||
// }
|
}
|
||||||
// }
|
return nil, false
|
||||||
// return true
|
}
|
||||||
// }
|
|
||||||
|
// ensureNotRevoked ensures the token is not revoked or superseded by a forced logout.
|
||||||
|
func ensureNotRevoked(c *fiber.Ctx, token string, verification *sso.VerificationResult) error {
|
||||||
|
revoker := session.GetRevocationStore()
|
||||||
|
if revoker == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
|
||||||
|
revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("auth: token revocation check failed")
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
if revoked {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if verification.UserID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutAt, err := revoker.UserLogoutTime(c.Context(), verification.UserID)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("auth: failed to load user logout marker")
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
if logoutAt.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := verification.Claims
|
||||||
|
if claims == nil || claims.IssuedAt == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
issuedAt := claims.IssuedAt.Time
|
||||||
|
// Treat tokens issued at or before the forced logout timestamp as invalid.
|
||||||
|
if !issuedAt.After(logoutAt) {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bearerToken extracts a Bearer token from the Authorization header using
|
||||||
|
// case-insensitive scheme matching and tolerant whitespace handling.
|
||||||
|
func bearerToken(c *fiber.Ctx) string {
|
||||||
|
parts := strings.Fields(c.Get("Authorization"))
|
||||||
|
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
|
||||||
|
return strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAllScopes(have, required []string) bool {
|
||||||
|
if len(required) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
set := make(map[string]struct{}, len(have))
|
||||||
|
for _, s := range have {
|
||||||
|
s = strings.ToLower(strings.TrimSpace(s))
|
||||||
|
if s != "" {
|
||||||
|
set[s] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range required {
|
||||||
|
r = strings.ToLower(strings.TrimSpace(r))
|
||||||
|
if r == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := set[r]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,3 +24,24 @@ func LimiterConfig() fiber.Handler {
|
|||||||
SkipSuccessfulRequests: true,
|
SkipSuccessfulRequests: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewLimiter(max int, expiration time.Duration) fiber.Handler {
|
||||||
|
if max <= 0 {
|
||||||
|
max = 10
|
||||||
|
}
|
||||||
|
if expiration <= 0 {
|
||||||
|
expiration = time.Minute
|
||||||
|
}
|
||||||
|
return limiter.New(limiter.Config{
|
||||||
|
Max: max,
|
||||||
|
Expiration: expiration,
|
||||||
|
LimitReached: func(c *fiber.Ctx) error {
|
||||||
|
return c.Status(fiber.StatusTooManyRequests).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusTooManyRequests,
|
||||||
|
Status: "error",
|
||||||
|
Message: "Too many requests, please try again later",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
||||||
|
func RequirePermissions(perms ...string) fiber.Handler {
|
||||||
|
required := canonicalPermissions(perms)
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
if len(required) == 0 {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
userPerms := ctx.permissionSet()
|
||||||
|
if len(userPerms) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range required {
|
||||||
|
if _, has := userPerms[perm]; !has {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPermission reports whether the current request context includes the given permission.
|
||||||
|
func HasPermission(c *fiber.Ctx, perm string) bool {
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
perm = canonicalPermission(perm)
|
||||||
|
if perm == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, has := ctx.permissionSet()[perm]
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthContext) permissionSet() map[string]struct{} {
|
||||||
|
if a == nil || a.Permissions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.Permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermissions(perms []string) []string {
|
||||||
|
out := make([]string, 0, len(perms))
|
||||||
|
seen := make(map[string]struct{}, len(perms))
|
||||||
|
for _, perm := range perms {
|
||||||
|
if canonical := canonicalPermission(perm); canonical != "" {
|
||||||
|
if _, ok := seen[canonical]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[canonical] = struct{}{}
|
||||||
|
out = append(out, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermission(perm string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(perm))
|
||||||
|
}
|
||||||
@@ -16,6 +16,10 @@ func JSONBody() fiber.Handler {
|
|||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(c.Path(), "/api/sso/users/sync") {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
body := c.Body()
|
body := c.Body()
|
||||||
if len(body) == 0 {
|
if len(body) == 0 {
|
||||||
return c.Next()
|
return c.Next()
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, v
|
|||||||
|
|
||||||
ProductWarehouseRoutes(router, userService, productWarehouseService)
|
ProductWarehouseRoutes(router, userService, productWarehouseService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package productWarehouses
|
package productWarehouses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/controllers"
|
||||||
productWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
|
productWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func ProductWarehouseRoutes(v1 fiber.Router, u user.UserService, s productWareho
|
|||||||
ctrl := controller.NewProductWarehouseController(s)
|
ctrl := controller.NewProductWarehouseController(s)
|
||||||
|
|
||||||
route := v1.Group("/product-warehouses")
|
route := v1.Group("/product-warehouses")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
|
|
||||||
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
|
adjustments "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments"
|
||||||
|
productWarehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses"
|
||||||
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
|
transfers "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers"
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package transfers
|
package transfers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/controllers"
|
||||||
transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
transfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
|
|||||||
ctrl := controller.NewTransferController(s)
|
ctrl := controller.NewTransferController(s)
|
||||||
|
|
||||||
route := v1.Group("/transfers")
|
route := v1.Group("/transfers")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (AreaModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *val
|
|||||||
|
|
||||||
AreaRoutes(router, userService, areaService)
|
AreaRoutes(router, userService, areaService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package areas
|
package areas
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/controllers"
|
||||||
area "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
|
area "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func AreaRoutes(v1 fiber.Router, u user.UserService, s area.AreaService) {
|
|||||||
ctrl := controller.NewAreaController(s)
|
ctrl := controller.NewAreaController(s)
|
||||||
|
|
||||||
route := v1.Group("/areas")
|
route := v1.Group("/areas")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (BankModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *val
|
|||||||
|
|
||||||
BankRoutes(router, userService, bankService)
|
BankRoutes(router, userService, bankService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package banks
|
package banks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/controllers"
|
||||||
bank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
|
bank "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func BankRoutes(v1 fiber.Router, u user.UserService, s bank.BankService) {
|
|||||||
ctrl := controller.NewBankController(s)
|
ctrl := controller.NewBankController(s)
|
||||||
|
|
||||||
route := v1.Group("/banks")
|
route := v1.Group("/banks")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (CustomerModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
|
|
||||||
CustomerRoutes(router, userService, customerService)
|
CustomerRoutes(router, userService, customerService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package customers
|
package customers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/controllers"
|
||||||
customer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
|
customer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func CustomerRoutes(v1 fiber.Router, u user.UserService, s customer.CustomerServ
|
|||||||
ctrl := controller.NewCustomerController(s)
|
ctrl := controller.NewCustomerController(s)
|
||||||
|
|
||||||
route := v1.Group("/customers")
|
route := v1.Group("/customers")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package fcrs
|
package fcrs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/controllers"
|
||||||
fcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
|
fcr "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func FcrRoutes(v1 fiber.Router, u user.UserService, s fcr.FcrService) {
|
|||||||
ctrl := controller.NewFcrController(s)
|
ctrl := controller.NewFcrController(s)
|
||||||
|
|
||||||
route := v1.Group("/fcrs")
|
route := v1.Group("/fcrs")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ func ToFlockListDTO(e entity.Flock) FlockListDTO {
|
|||||||
|
|
||||||
return FlockListDTO{
|
return FlockListDTO{
|
||||||
FlockBaseDTO: ToFlockBaseDTO(e),
|
FlockBaseDTO: ToFlockBaseDTO(e),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package flocks
|
package flocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/controllers"
|
||||||
flock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/services"
|
flock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func FlockRoutes(v1 fiber.Router, u user.UserService, s flock.FlockService) {
|
|||||||
ctrl := controller.NewFlockController(s)
|
ctrl := controller.NewFlockController(s)
|
||||||
|
|
||||||
route := v1.Group("/flocks")
|
route := v1.Group("/flocks")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3"`
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (KandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
|
|
||||||
KandangRoutes(router, userService, kandangService)
|
KandangRoutes(router, userService, kandangService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package kandangs
|
package kandangs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
|
||||||
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
|
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService
|
|||||||
ctrl := controller.NewKandangController(s)
|
ctrl := controller.NewKandangController(s)
|
||||||
|
|
||||||
route := v1.Group("/kandangs")
|
route := v1.Group("/kandangs")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func NewKandangService(repo repository.KandangRepository, validate *validator.Va
|
|||||||
|
|
||||||
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db.Preload("CreatedUser").Preload("Location").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
|
return db.Preload("CreatedUser").Preload("Location").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
|
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
|
||||||
@@ -132,11 +132,11 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
|
|
||||||
//TODO: created by dummy
|
//TODO: created by dummy
|
||||||
createBody := &entity.Kandang{
|
createBody := &entity.Kandang{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
LocationId: req.LocationId,
|
LocationId: req.LocationId,
|
||||||
Status: status,
|
Status: status,
|
||||||
PicId: req.PicId,
|
PicId: req.PicId,
|
||||||
CreatedBy: 1,
|
CreatedBy: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (LocationModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
|
|
||||||
LocationRoutes(router, userService, locationService)
|
LocationRoutes(router, userService, locationService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package locations
|
package locations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/controllers"
|
||||||
location "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
|
location "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func LocationRoutes(v1 fiber.Router, u user.UserService, s location.LocationServ
|
|||||||
ctrl := controller.NewLocationController(s)
|
ctrl := controller.NewLocationController(s)
|
||||||
|
|
||||||
route := v1.Group("/locations")
|
route := v1.Group("/locations")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (NonstockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
|
|
||||||
NonstockRoutes(router, userService, nonstockService)
|
NonstockRoutes(router, userService, nonstockService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package nonstocks
|
package nonstocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/controllers"
|
||||||
nonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
|
nonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func NonstockRoutes(v1 fiber.Router, u user.UserService, s nonstock.NonstockServ
|
|||||||
ctrl := controller.NewNonstockController(s)
|
ctrl := controller.NewNonstockController(s)
|
||||||
|
|
||||||
route := v1.Group("/nonstocks")
|
route := v1.Group("/nonstocks")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package productcategories
|
package productcategories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/controllers"
|
||||||
productCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
|
productCategory "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func ProductCategoryRoutes(v1 fiber.Router, u user.UserService, s productCategor
|
|||||||
ctrl := controller.NewProductCategoryController(s)
|
ctrl := controller.NewProductCategoryController(s)
|
||||||
|
|
||||||
route := v1.Group("/product-categories")
|
route := v1.Group("/product-categories")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (ProductModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
|
|
||||||
ProductRoutes(router, userService, productService)
|
ProductRoutes(router, userService, productService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package products
|
package products
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/controllers"
|
||||||
product "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
|
product "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func ProductRoutes(v1 fiber.Router, u user.UserService, s product.ProductService
|
|||||||
ctrl := controller.NewProductController(s)
|
ctrl := controller.NewProductController(s)
|
||||||
|
|
||||||
route := v1.Group("/products")
|
route := v1.Group("/products")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks"
|
banks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks"
|
||||||
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
|
customers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers"
|
||||||
fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs"
|
fcrs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs"
|
||||||
|
flocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks"
|
||||||
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
|
kandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs"
|
||||||
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
|
locations "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations"
|
||||||
nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks"
|
nonstocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks"
|
||||||
@@ -19,7 +20,6 @@ import (
|
|||||||
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
|
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
|
||||||
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
|
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
|
||||||
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
|
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
|
||||||
flocks "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks"
|
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (SupplierModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
|
|
||||||
SupplierRoutes(router, userService, supplierService)
|
SupplierRoutes(router, userService, supplierService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package suppliers
|
package suppliers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/controllers"
|
||||||
supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services"
|
supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierServ
|
|||||||
ctrl := controller.NewSupplierController(s)
|
ctrl := controller.NewSupplierController(s)
|
||||||
|
|
||||||
route := v1.Group("/suppliers")
|
route := v1.Group("/suppliers")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (UomModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *vali
|
|||||||
|
|
||||||
UomRoutes(router, userService, uomService)
|
UomRoutes(router, userService, uomService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package uoms
|
package uoms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/controllers"
|
||||||
uom "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/services"
|
uom "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func UomRoutes(v1 fiber.Router, u user.UserService, s uom.UomService) {
|
|||||||
ctrl := controller.NewUomController(s)
|
ctrl := controller.NewUomController(s)
|
||||||
|
|
||||||
route := v1.Group("/uoms")
|
route := v1.Group("/uoms")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -23,4 +23,3 @@ func (WarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
|
|
||||||
WarehouseRoutes(router, userService, warehouseService)
|
WarehouseRoutes(router, userService, warehouseService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package warehouses
|
package warehouses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/controllers"
|
||||||
warehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
|
warehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func WarehouseRoutes(v1 fiber.Router, u user.UserService, s warehouse.WarehouseS
|
|||||||
ctrl := controller.NewWarehouseController(s)
|
ctrl := controller.NewWarehouseController(s)
|
||||||
|
|
||||||
route := v1.Group("/warehouses")
|
route := v1.Group("/warehouses")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package chickins
|
package chickins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/controllers"
|
||||||
chickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
|
chickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
|
|||||||
ctrl := controller.NewChickinController(s)
|
ctrl := controller.NewChickinController(s)
|
||||||
|
|
||||||
route := v1.Group("/chickins")
|
route := v1.Group("/chickins")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package project_flocks
|
package project_flocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers"
|
||||||
projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -12,13 +12,8 @@ import (
|
|||||||
func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) {
|
func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) {
|
||||||
ctrl := controller.NewProjectflockController(s)
|
ctrl := controller.NewProjectflockController(s)
|
||||||
|
|
||||||
route := v1.Group("/project_flocks")
|
route := v1.Group("/project-flocks")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
@@ -28,5 +23,5 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
|||||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||||
route.Post("/approvals", ctrl.Approval)
|
route.Post("/approvals", ctrl.Approval)
|
||||||
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
|
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||||
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
@@ -158,6 +159,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID, err := actorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
cat := strings.ToUpper(req.Category)
|
cat := strings.ToUpper(req.Category)
|
||||||
if !utils.IsValidProjectFlockCategory(cat) {
|
if !utils.IsValidProjectFlockCategory(cat) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
|
||||||
@@ -182,7 +188,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
|
|
||||||
canonicalBase := baseName
|
canonicalBase := baseName
|
||||||
if s.FlockRepo != nil {
|
if s.FlockRepo != nil {
|
||||||
baseFlock, err := s.ensureFlockByName(c.Context(), baseName)
|
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -212,7 +218,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
Category: cat,
|
Category: cat,
|
||||||
FcrId: req.FcrId,
|
FcrId: req.FcrId,
|
||||||
LocationId: req.LocationId,
|
LocationId: req.LocationId,
|
||||||
CreatedBy: 1,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
@@ -237,7 +243,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) //TODO: Change From Auth
|
|
||||||
action := entity.ApprovalActionCreated
|
action := entity.ApprovalActionCreated
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
_, err = approvalSvcTx.CreateApproval(
|
_, err = approvalSvcTx.CreateApproval(
|
||||||
@@ -271,6 +276,11 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID, err := actorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||||
@@ -293,7 +303,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
|||||||
}
|
}
|
||||||
canonicalBase := trimmed
|
canonicalBase := trimmed
|
||||||
if s.FlockRepo != nil {
|
if s.FlockRepo != nil {
|
||||||
flockEntity, err := s.ensureFlockByName(c.Context(), trimmed)
|
flockEntity, err := s.ensureFlockByName(c.Context(), actorID, trimmed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -452,7 +462,6 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasChanges {
|
if hasChanges {
|
||||||
actorID := uint(1) //TODO: Change From Auth
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
if approvalSvc != nil {
|
if approvalSvc != nil {
|
||||||
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
|
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
|
||||||
@@ -506,7 +515,11 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
actorID := uint(1) // TODO: change from auth context
|
actorID, err := actorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var action entity.ApprovalAction
|
var action entity.ApprovalAction
|
||||||
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||||
case string(entity.ApprovalActionRejected):
|
case string(entity.ApprovalActionRejected):
|
||||||
@@ -527,7 +540,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
|
|||||||
step = utils.ProjectFlockStepAktif
|
step = utils.ProjectFlockStepAktif
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
|
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
|
||||||
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
|
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
|
||||||
@@ -814,7 +827,7 @@ func (s projectflockService) generateSequentialFlockName(ctx context.Context, re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) ensureFlockByName(ctx context.Context, name string) (*entity.Flock, error) {
|
func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint, name string) (*entity.Flock, error) {
|
||||||
trimmed := strings.TrimSpace(name)
|
trimmed := strings.TrimSpace(name)
|
||||||
if trimmed == "" {
|
if trimmed == "" {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
|
||||||
@@ -831,7 +844,7 @@ func (s projectflockService) ensureFlockByName(ctx context.Context, name string)
|
|||||||
|
|
||||||
newFlock := &entity.Flock{
|
newFlock := &entity.Flock{
|
||||||
Name: trimmed,
|
Name: trimmed,
|
||||||
CreatedBy: 1, // TODO: replace with authenticated user
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
|
if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
@@ -950,3 +963,11 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
|
|||||||
}
|
}
|
||||||
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||||
|
user, ok := authmiddleware.AuthenticatedUser(c)
|
||||||
|
if !ok || user == nil || user.Id == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
return user.Id, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package recordings
|
||||||
|
|
||||||
|
const (
|
||||||
|
PermissionRecordingRead = "recording.read"
|
||||||
|
PermissionRecordingCreate = "recording.write"
|
||||||
|
PermissionRecordingUpdate = "recording.update"
|
||||||
|
PermissionRecordingDelete = "recording.delete"
|
||||||
|
)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package recordings
|
package recordings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/controllers"
|
||||||
recording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
recording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,12 +13,7 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
|
|||||||
ctrl := controller.NewRecordingController(s)
|
ctrl := controller.NewRecordingController(s)
|
||||||
|
|
||||||
route := v1.Group("/recordings")
|
route := v1.Group("/recordings")
|
||||||
|
route.Use(m.Auth(u))
|
||||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
|
||||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
|
||||||
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
|
||||||
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
|
||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/next-day", ctrl.GetNextDay)
|
route.Get("/next-day", ctrl.GetNextDay)
|
||||||
|
|||||||
@@ -0,0 +1,706 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/secure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller manages the SSO start & callback flow using PKCE.
|
||||||
|
type Controller struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
store *session.Store
|
||||||
|
revoker *session.RevocationStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewController(client *http.Client, store *session.Store, revoker *session.RevocationStore) *Controller {
|
||||||
|
return &Controller{httpClient: client, store: store, revoker: revoker}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start handles GET /sso/start requests and redirects users to the central SSO authorize endpoint.
|
||||||
|
func (h *Controller) Start(c *fiber.Ctx) error {
|
||||||
|
requestedAlias := normalizeClientParam(c.Query("client"))
|
||||||
|
if requestedAlias == "" {
|
||||||
|
requestedAlias = normalizeClientParam(c.Query("client_id"))
|
||||||
|
}
|
||||||
|
if requestedAlias == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "missing client")
|
||||||
|
}
|
||||||
|
|
||||||
|
alias, cfg, ok := findSSOClientConfig(requestedAlias)
|
||||||
|
if !ok || cfg.PublicID == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "unknown client")
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizeEndpoint := strings.TrimSpace(config.SSOAuthorizeURL)
|
||||||
|
if authorizeEndpoint == "" {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "authorize endpoint not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := secure.RandomString(48)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("generate state failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, err := secure.RandomString(32)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("generate nonce failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
codeVerifier, err := secure.PKCECodeVerifier(96)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("generate code verifier failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := sha256.Sum256([]byte(codeVerifier))
|
||||||
|
challenge := secure.Base64URLEncode(digest[:])
|
||||||
|
|
||||||
|
authorizeURL, err := url.Parse(authorizeEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "invalid authorize endpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := cfg.Scope
|
||||||
|
if scope == "" {
|
||||||
|
scope = "openid profile"
|
||||||
|
}
|
||||||
|
if !strings.Contains(" "+scope+" ", " openid ") {
|
||||||
|
scope = scope + " openid"
|
||||||
|
}
|
||||||
|
|
||||||
|
rawReturn := strings.TrimSpace(c.Query("return_to"))
|
||||||
|
if rawReturn == "" {
|
||||||
|
rawReturn = cfg.DefaultReturnURI
|
||||||
|
}
|
||||||
|
|
||||||
|
returnTo, err := normalizeReturnTarget(rawReturn, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
query := authorizeURL.Query()
|
||||||
|
query.Set("response_type", "code")
|
||||||
|
query.Set("client_id", cfg.PublicID)
|
||||||
|
query.Set("redirect_uri", cfg.RedirectURI)
|
||||||
|
query.Set("scope", strings.TrimSpace(scope))
|
||||||
|
query.Set("state", state)
|
||||||
|
query.Set("code_challenge", challenge)
|
||||||
|
query.Set("code_challenge_method", "S256")
|
||||||
|
query.Set("nonce", nonce)
|
||||||
|
// if prompt := strings.TrimSpace(cfg.Prompt); prompt != "" {
|
||||||
|
// query.Set("prompt", prompt)
|
||||||
|
// }
|
||||||
|
if extraPrompt := strings.TrimSpace(c.Query("prompt")); extraPrompt != "" {
|
||||||
|
query.Set("prompt", extraPrompt)
|
||||||
|
}
|
||||||
|
authorizeURL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
payload := &session.PKCESession{
|
||||||
|
CodeVerifier: codeVerifier,
|
||||||
|
Nonce: nonce,
|
||||||
|
ClientAlias: alias,
|
||||||
|
ClientID: cfg.PublicID,
|
||||||
|
RedirectURI: cfg.RedirectURI,
|
||||||
|
Scope: strings.TrimSpace(scope),
|
||||||
|
ReturnTo: returnTo,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.store.Save(c.Context(), state, payload); err != nil {
|
||||||
|
utils.Log.Errorf("store pkce session failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Log.WithFields(logrus.Fields{
|
||||||
|
"client": alias,
|
||||||
|
"state": state,
|
||||||
|
"return_to": returnTo,
|
||||||
|
}).Info("sso start redirect")
|
||||||
|
|
||||||
|
return c.Redirect(authorizeURL.String(), fiber.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback handles the redirect from SSO containing the authorization code.
|
||||||
|
func (h *Controller) Callback(c *fiber.Ctx) error {
|
||||||
|
state := strings.TrimSpace(c.Query("state"))
|
||||||
|
code := strings.TrimSpace(c.Query("code"))
|
||||||
|
if state == "" || code == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "missing code or state")
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionData, err := h.store.Get(c.Context(), state)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("load pkce session failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to validate authorization state")
|
||||||
|
}
|
||||||
|
if sessionData == nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "authorization state not found or expired")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := h.store.Delete(context.Background(), state); err != nil {
|
||||||
|
utils.Log.Warnf("failed to delete pkce session: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tokenEndpoint := strings.TrimSpace(config.SSOTokenURL)
|
||||||
|
if tokenEndpoint == "" {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "token endpoint not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("grant_type", "authorization_code")
|
||||||
|
form.Set("code", code)
|
||||||
|
form.Set("code_verifier", sessionData.CodeVerifier)
|
||||||
|
form.Set("redirect_uri", sessionData.RedirectURI)
|
||||||
|
form.Set("client_id", sessionData.ClientID)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(c.Context(), http.MethodPost, tokenEndpoint, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to create token request")
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := h.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("token request failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusBadGateway, "failed to exchange authorization code")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
utils.Log.Warnf("token response status %d", resp.StatusCode)
|
||||||
|
return fiber.NewError(fiber.StatusBadGateway, "token exchange rejected")
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
IDToken string `json:"id_token"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Description string `json:"error_description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadGateway, "invalid token response")
|
||||||
|
}
|
||||||
|
if tokenResp.Error != "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadGateway, tokenResp.Description)
|
||||||
|
}
|
||||||
|
if tokenResp.AccessToken == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadGateway, "missing access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
verification, err := sso.VerifyAccessToken(tokenResp.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("access token verification failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "invalid access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare cookies
|
||||||
|
issueCookies(c, tokenResp, verification)
|
||||||
|
|
||||||
|
redirectTarget := sessionData.ReturnTo
|
||||||
|
if redirectTarget == "" {
|
||||||
|
redirectTarget = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Log.WithFields(logrus.Fields{
|
||||||
|
"client": sessionData.ClientAlias,
|
||||||
|
"user_id": verification.UserID,
|
||||||
|
"return_to": redirectTarget,
|
||||||
|
}).Info("sso callback successful")
|
||||||
|
|
||||||
|
return c.Redirect(redirectTarget, fiber.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInfo proxies the user profile from the central SSO so the frontend can obtain
|
||||||
|
// enriched user metadata (roles, permissions, etc.) without exposing tokens to the browser.
|
||||||
|
func (h *Controller) UserInfo(c *fiber.Ctx) error {
|
||||||
|
accessName := config.SSOAccessCookieName
|
||||||
|
if accessName == "" {
|
||||||
|
accessName = "sso_access"
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimSpace(c.Cookies(accessName))
|
||||||
|
tokenFromCookie := token != ""
|
||||||
|
|
||||||
|
if !tokenFromCookie {
|
||||||
|
authHeader := strings.TrimSpace(c.Get("Authorization"))
|
||||||
|
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
|
||||||
|
token = strings.TrimSpace(authHeader[7:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == "" {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
if revoker := session.GetRevocationStore(); revoker != nil {
|
||||||
|
if fingerprint := session.TokenFingerprint(token); fingerprint != "" {
|
||||||
|
revoked, err := revoker.IsRevoked(c.Context(), fingerprint)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("failed to check token revocation for userinfo")
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
||||||
|
}
|
||||||
|
if revoked {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := sso.VerifyAccessToken(token); err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("access token verification failed for userinfo")
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := strings.TrimSpace(config.SSOGetMeURL)
|
||||||
|
if endpoint == "" {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "userinfo endpoint not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("failed to build userinfo request: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to prepare userinfo request")
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
// SSO /auth/get-me expects the access cookie; add Authorization as well for compatibility.
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
if tokenFromCookie {
|
||||||
|
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", accessName, token))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("userinfo request failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusBadGateway, "failed to fetch user profile")
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
utils.Log.WithFields(logrus.Fields{"status": resp.StatusCode}).Info("sso userinfo response")
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Errorf("failed to read userinfo response: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusBadGateway, "invalid user profile response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if sanitized, perms, ok := sanitizeUserInfoPayload(body); ok {
|
||||||
|
// if caps := capabilities.FromPermissions(perms); len(caps) > 0 {
|
||||||
|
// injectCapabilities(sanitized, caps)
|
||||||
|
// }
|
||||||
|
// return c.Status(resp.StatusCode).JSON(sanitized)
|
||||||
|
// }
|
||||||
|
|
||||||
|
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||||
|
c.Set("Content-Type", ct)
|
||||||
|
} else {
|
||||||
|
c.Type("json")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(resp.StatusCode).Send(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout clears SSO cookies and removes any leftover PKCE session state.
|
||||||
|
func (h *Controller) Logout(c *fiber.Ctx) error {
|
||||||
|
requestedAlias := normalizeClientParam(c.Query("client"))
|
||||||
|
if requestedAlias == "" {
|
||||||
|
requestedAlias = normalizeClientParam(c.Query("client_id"))
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
alias string
|
||||||
|
cfg config.SSOClientConfig
|
||||||
|
hasClientInfo bool
|
||||||
|
)
|
||||||
|
if requestedAlias != "" {
|
||||||
|
alias, cfg, hasClientInfo = findSSOClientConfig(requestedAlias)
|
||||||
|
}
|
||||||
|
|
||||||
|
accessName := resolveSSOCookieName(config.SSOAccessCookieName, "access")
|
||||||
|
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
|
||||||
|
|
||||||
|
var accessToken, refreshToken string
|
||||||
|
if accessName != "" {
|
||||||
|
accessToken = strings.TrimSpace(c.Cookies(accessName))
|
||||||
|
}
|
||||||
|
if refreshName != "" {
|
||||||
|
refreshToken = strings.TrimSpace(c.Cookies(refreshName))
|
||||||
|
}
|
||||||
|
hadAccessCookie := accessToken != ""
|
||||||
|
hadRefreshCookie := refreshToken != ""
|
||||||
|
|
||||||
|
state := strings.TrimSpace(c.Query("state"))
|
||||||
|
if state != "" {
|
||||||
|
if err := h.store.Delete(c.Context(), state); err != nil {
|
||||||
|
utils.Log.Warnf("failed to delete pkce session during logout: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hadAccessCookie && !hadRefreshCookie && state == "" {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "not authenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hadAccessCookie {
|
||||||
|
if verification, err := sso.VerifyAccessToken(accessToken); err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("failed to verify access token during logout")
|
||||||
|
} else {
|
||||||
|
if revoker := session.GetRevocationStore(); revoker != nil {
|
||||||
|
if err := revoker.MarkUserLogout(c.Context(), verification.UserID, time.Now().UTC()); err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("failed to mark user logout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.revokeToken(c.Context(), accessToken, verification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if refreshToken != "" {
|
||||||
|
h.revokeRefreshToken(c.Context(), refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSSOCookie(c, accessName)
|
||||||
|
clearSSOCookie(c, refreshName)
|
||||||
|
|
||||||
|
redirectTarget := ""
|
||||||
|
rawReturn := strings.TrimSpace(c.Query("return_to"))
|
||||||
|
if hasClientInfo {
|
||||||
|
if rawReturn == "" {
|
||||||
|
rawReturn = cfg.DefaultReturnURI
|
||||||
|
}
|
||||||
|
if normalized, err := normalizeReturnTarget(rawReturn, cfg); err == nil {
|
||||||
|
redirectTarget = normalized
|
||||||
|
} else if rawReturn != "" {
|
||||||
|
utils.Log.WithError(err).Warn("invalid return_to during logout")
|
||||||
|
}
|
||||||
|
} else if rawReturn != "" {
|
||||||
|
if strings.HasPrefix(rawReturn, "/") && !strings.HasPrefix(rawReturn, "//") {
|
||||||
|
redirectTarget = rawReturn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Log.WithFields(logrus.Fields{
|
||||||
|
"client": alias,
|
||||||
|
"state": state,
|
||||||
|
"redirect": redirectTarget,
|
||||||
|
}).Info("sso logout completed")
|
||||||
|
|
||||||
|
if redirectTarget != "" {
|
||||||
|
return c.Redirect(redirectTarget, fiber.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "signed out"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Controller) revokeToken(ctx context.Context, token string, verification *sso.VerificationResult) {
|
||||||
|
if h.revoker == nil || verification == nil || verification.Claims == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fingerprint := session.TokenFingerprint(token)
|
||||||
|
if fingerprint == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if verification.Claims.ExpiresAt == nil {
|
||||||
|
utils.Log.Warn("access token missing expiry claim")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ttl := time.Until(verification.Claims.ExpiresAt.Time)
|
||||||
|
if ttl <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ttl < time.Second {
|
||||||
|
ttl = time.Second
|
||||||
|
}
|
||||||
|
if err := h.revoker.Revoke(ctx, fingerprint, ttl); err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("failed to revoke access token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Controller) revokeRefreshToken(ctx context.Context, token string) {
|
||||||
|
if h.revoker == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fingerprint := session.TokenFingerprint(token)
|
||||||
|
if fingerprint == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const refreshTTL = 30 * 24 * time.Hour
|
||||||
|
if err := h.revoker.Revoke(ctx, fingerprint, refreshTTL); err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("failed to revoke refresh token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func issueCookies(c *fiber.Ctx, tokenResp struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
IDToken string `json:"id_token"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Description string `json:"error_description"`
|
||||||
|
}, verification *sso.VerificationResult) {
|
||||||
|
if revoker := session.GetRevocationStore(); revoker != nil && verification != nil {
|
||||||
|
if err := revoker.ClearUserLogout(c.Context(), verification.UserID); err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("failed to clear logout marker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accessName := resolveSSOCookieName(config.SSOAccessCookieName, "access")
|
||||||
|
refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh")
|
||||||
|
maxAge := tokenResp.ExpiresIn
|
||||||
|
if maxAge <= 0 {
|
||||||
|
maxAge = int(15 * time.Minute.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
sameSite := config.SSOCookieSameSite
|
||||||
|
if sameSite == "" {
|
||||||
|
sameSite = "Lax"
|
||||||
|
}
|
||||||
|
|
||||||
|
cookieDomain := config.SSOCookieDomain
|
||||||
|
|
||||||
|
cookieAccess := &fiber.Cookie{
|
||||||
|
Name: accessName,
|
||||||
|
Value: tokenResp.AccessToken,
|
||||||
|
Path: "/",
|
||||||
|
Domain: cookieDomain,
|
||||||
|
HTTPOnly: true,
|
||||||
|
Secure: config.SSOCookieSecure,
|
||||||
|
SameSite: sameSite,
|
||||||
|
MaxAge: maxAge,
|
||||||
|
}
|
||||||
|
c.Cookie(cookieAccess)
|
||||||
|
if tokenResp.RefreshToken != "" {
|
||||||
|
cookieRefresh := &fiber.Cookie{
|
||||||
|
Name: refreshName,
|
||||||
|
Value: tokenResp.RefreshToken,
|
||||||
|
Path: "/",
|
||||||
|
Domain: cookieDomain,
|
||||||
|
HTTPOnly: true,
|
||||||
|
Secure: config.SSOCookieSecure,
|
||||||
|
SameSite: sameSite,
|
||||||
|
MaxAge: int((time.Hour * 24 * 30).Seconds()),
|
||||||
|
}
|
||||||
|
c.Cookie(cookieRefresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: expose limited info via headers for FE debugging (avoid tokens)
|
||||||
|
c.Set("X-Auth-User", fmt.Sprintf("%d", verification.UserID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearSSOCookie(c *fiber.Ctx, name string) {
|
||||||
|
if name == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sameSite := config.SSOCookieSameSite
|
||||||
|
if sameSite == "" {
|
||||||
|
sameSite = "Lax"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Cookie(&fiber.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: "",
|
||||||
|
Path: "/",
|
||||||
|
Domain: config.SSOCookieDomain,
|
||||||
|
HTTPOnly: true,
|
||||||
|
Secure: config.SSOCookieSecure,
|
||||||
|
SameSite: sameSite,
|
||||||
|
Expires: time.Unix(0, 0),
|
||||||
|
MaxAge: -1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSSOCookieName(configuredName, fallback string) string {
|
||||||
|
name := strings.TrimSpace(configuredName)
|
||||||
|
if name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeClientParam(raw string) string {
|
||||||
|
value := strings.TrimSpace(raw)
|
||||||
|
if value == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if idx := strings.Index(value, "|"); idx >= 0 {
|
||||||
|
value = value[:idx]
|
||||||
|
}
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
return strings.ToLower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeUserInfoPayload(body []byte) (map[string]any, []string, bool) {
|
||||||
|
if len(body) == 0 {
|
||||||
|
return map[string]any{}, nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload any
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
perms := collectPermissionNames(payload)
|
||||||
|
|
||||||
|
sensitive := map[string]struct{}{
|
||||||
|
"roles": {},
|
||||||
|
"permissions": {},
|
||||||
|
}
|
||||||
|
payload = scrubSensitiveKeys(payload, sensitive)
|
||||||
|
|
||||||
|
sanitized, ok := payload.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
sanitized = map[string]any{"data": payload}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized, perms, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrubSensitiveKeys(value any, sensitive map[string]struct{}) any {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for key, val := range v {
|
||||||
|
if _, ok := sensitive[strings.ToLower(key)]; ok {
|
||||||
|
delete(v, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v[key] = scrubSensitiveKeys(val, sensitive)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
case []any:
|
||||||
|
for i, item := range v {
|
||||||
|
v[i] = scrubSensitiveKeys(item, sensitive)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectPermissionNames(value any) []string {
|
||||||
|
names := make(map[string]struct{})
|
||||||
|
collectPermissionRec(value, names)
|
||||||
|
out := make([]string, 0, len(names))
|
||||||
|
for name := range names {
|
||||||
|
out = append(out, name)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectPermissionRec(value any, acc map[string]struct{}) {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for key, val := range v {
|
||||||
|
if strings.EqualFold(key, "permissions") {
|
||||||
|
if arr, ok := val.([]any); ok {
|
||||||
|
for _, item := range arr {
|
||||||
|
if perm, ok := item.(map[string]any); ok {
|
||||||
|
if name, ok := perm["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||||
|
acc[strings.ToLower(strings.TrimSpace(name))] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collectPermissionRec(val, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, item := range v {
|
||||||
|
collectPermissionRec(item, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func injectCapabilities(payload map[string]any, caps map[string]bool) {
|
||||||
|
if len(caps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data, ok := payload["data"].(map[string]any); ok {
|
||||||
|
data["capabilities"] = caps
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload["capabilities"] = caps
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSSOClientConfig(requestedAlias string) (string, config.SSOClientConfig, bool) {
|
||||||
|
if requestedAlias == "" {
|
||||||
|
return "", config.SSOClientConfig{}, false
|
||||||
|
}
|
||||||
|
if cfg, ok := config.SSOClients[requestedAlias]; ok && strings.TrimSpace(cfg.PublicID) != "" {
|
||||||
|
return requestedAlias, cfg, true
|
||||||
|
}
|
||||||
|
for alias, cfg := range config.SSOClients {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(cfg.PublicID), requestedAlias) && strings.TrimSpace(cfg.PublicID) != "" {
|
||||||
|
return alias, cfg, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", config.SSOClientConfig{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeReturnTarget(returnTo string, cfg config.SSOClientConfig) (string, error) {
|
||||||
|
returnTo = strings.TrimSpace(returnTo)
|
||||||
|
if returnTo == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(returnTo, "//") {
|
||||||
|
return "", fmt.Errorf("invalid return_to")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(returnTo, "/") {
|
||||||
|
return returnTo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := url.Parse(returnTo)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid return_to")
|
||||||
|
}
|
||||||
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||||
|
return "", fmt.Errorf("invalid return_to scheme")
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedOrigins := make(map[string]struct{})
|
||||||
|
if cfg.DefaultReturnURI != "" {
|
||||||
|
if u, err := url.Parse(cfg.DefaultReturnURI); err == nil && u.Host != "" {
|
||||||
|
allowedOrigins[u.Scheme+"://"+u.Host] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, origin := range cfg.AllowedReturnOrigins {
|
||||||
|
origin = strings.TrimSpace(origin)
|
||||||
|
if origin == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if u, err := url.Parse(origin); err == nil && u.Host != "" && (u.Scheme == "http" || u.Scheme == "https") {
|
||||||
|
allowedOrigins[u.Scheme+"://"+u.Host] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allowedOrigins) > 0 {
|
||||||
|
origin := parsed.Scheme + "://" + parsed.Host
|
||||||
|
if _, ok := allowedOrigins[origin]; !ok {
|
||||||
|
return "", fmt.Errorf("return_to origin not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.String(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
userRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headerClient = "X-Sync-Client"
|
||||||
|
headerTimestamp = "X-Sync-Timestamp"
|
||||||
|
headerNonce = "X-Sync-Nonce"
|
||||||
|
headerSignature = "X-Sync-Signature"
|
||||||
|
defaultDrift = 2 * time.Minute
|
||||||
|
defaultNonceTTL = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserSyncController handles incoming user management events from the central SSO service.
|
||||||
|
type UserSyncController struct {
|
||||||
|
validate *validator.Validate
|
||||||
|
repo userRepository.UserRepository
|
||||||
|
redis *redis.Client
|
||||||
|
clients map[string]config.SSOClientConfig
|
||||||
|
drift time.Duration
|
||||||
|
nonceTTL time.Duration
|
||||||
|
maxBodyBytes int
|
||||||
|
log *logrus.Logger
|
||||||
|
localNonces sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
type userSyncRequest struct {
|
||||||
|
Action string `json:"action" validate:"required,oneof=create update delete logout"`
|
||||||
|
PublicID string `json:"public_id" validate:"required"`
|
||||||
|
User userSyncUser `json:"user" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userSyncUser struct {
|
||||||
|
ID int64 `json:"id" validate:"required"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserSyncController(validate *validator.Validate, repo userRepository.UserRepository, redis *redis.Client, clients map[string]config.SSOClientConfig) *UserSyncController {
|
||||||
|
normalized := make(map[string]config.SSOClientConfig, len(clients))
|
||||||
|
for alias, cfg := range clients {
|
||||||
|
alias = strings.ToLower(strings.TrimSpace(alias))
|
||||||
|
normalized[alias] = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
drift := config.SSOUserSyncDrift
|
||||||
|
if drift <= 0 {
|
||||||
|
drift = defaultDrift
|
||||||
|
}
|
||||||
|
|
||||||
|
nonceTTL := config.SSOUserSyncNonceTTL
|
||||||
|
if nonceTTL <= 0 {
|
||||||
|
nonceTTL = defaultNonceTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
maxBody := config.SSOUserSyncMaxBodyBytes
|
||||||
|
if maxBody <= 0 {
|
||||||
|
maxBody = 32 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
log := utils.Log
|
||||||
|
if redis == nil {
|
||||||
|
log.Warn("SSO user sync nonce store fallback to in-memory cache; enable Redis for replay protection")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserSyncController{
|
||||||
|
validate: validate,
|
||||||
|
repo: repo,
|
||||||
|
redis: redis,
|
||||||
|
clients: normalized,
|
||||||
|
drift: drift,
|
||||||
|
nonceTTL: nonceTTL,
|
||||||
|
maxBodyBytes: maxBody,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) Sync(c *fiber.Ctx) error {
|
||||||
|
if ct := strings.TrimSpace(c.Get(fiber.HeaderContentType)); ct != "" && !strings.HasPrefix(strings.ToLower(ct), fiber.MIMEApplicationJSON) {
|
||||||
|
return fiber.NewError(fiber.StatusUnsupportedMediaType, "content-type must be application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
body := c.Body()
|
||||||
|
if h.maxBodyBytes > 0 && len(body) > h.maxBodyBytes {
|
||||||
|
return fiber.NewError(fiber.StatusRequestEntityTooLarge, "request body too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
alias, clientCfg, err := h.authenticate(c, body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(userSyncRequest)
|
||||||
|
if err := json.Unmarshal(body, req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Action = strings.ToLower(strings.TrimSpace(req.Action))
|
||||||
|
req.PublicID = strings.TrimSpace(req.PublicID)
|
||||||
|
req.User.Email = strings.TrimSpace(req.User.Email)
|
||||||
|
req.User.Name = strings.TrimSpace(req.User.Name)
|
||||||
|
|
||||||
|
if err := h.validate.Struct(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientCfg.PublicID != "" && req.PublicID != clientCfg.PublicID {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "public_id mismatch with configured client")
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Action == "create" || req.Action == "update" {
|
||||||
|
if req.User.Email == "" || req.User.Name == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "email and name are required for create/update actions")
|
||||||
|
}
|
||||||
|
if err := h.validate.Var(req.User.Email, "email"); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "invalid email format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.User.ID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "invalid user id")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Action {
|
||||||
|
case "create", "update":
|
||||||
|
return h.upsertUser(c, alias, req)
|
||||||
|
case "delete":
|
||||||
|
return h.removeUser(c, alias, req)
|
||||||
|
case "logout":
|
||||||
|
return h.logoutUser(c, alias, req)
|
||||||
|
default:
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "unsupported action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) authenticate(c *fiber.Ctx, body []byte) (string, config.SSOClientConfig, error) {
|
||||||
|
rawAlias := strings.TrimSpace(c.Get(headerClient))
|
||||||
|
if rawAlias == "" {
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing sync client header")
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasKey := strings.ToLower(rawAlias)
|
||||||
|
clientCfg, ok := h.clients[aliasKey]
|
||||||
|
if !ok {
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "unknown sync client")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.verifyAuthorization(c, aliasKey); err != nil {
|
||||||
|
return "", config.SSOClientConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := strings.TrimSpace(clientCfg.SyncSecret)
|
||||||
|
if secret == "" {
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "sync secret not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := strings.TrimSpace(c.Get(headerTimestamp))
|
||||||
|
nonce := strings.TrimSpace(c.Get(headerNonce))
|
||||||
|
signature := strings.TrimSpace(c.Get(headerSignature))
|
||||||
|
|
||||||
|
if timestamp == "" || nonce == "" || signature == "" {
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing signature headers")
|
||||||
|
}
|
||||||
|
if len(nonce) < 16 {
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "nonce too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := strconv.ParseInt(timestamp, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusBadRequest, "invalid timestamp")
|
||||||
|
}
|
||||||
|
|
||||||
|
msgTime := time.Unix(ts, 0).UTC()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
drift := now.Sub(msgTime)
|
||||||
|
if drift > h.drift || drift < -h.drift {
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "timestamp outside allowed window")
|
||||||
|
}
|
||||||
|
|
||||||
|
providedSig, err := decodeSignature(signature)
|
||||||
|
if err != nil {
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSignature := h.calculateSignature(secret, rawAlias, timestamp, nonce, body)
|
||||||
|
if !hmac.Equal(providedSig, expectedSignature) {
|
||||||
|
bodyHash := sha256.Sum256(body)
|
||||||
|
h.log.WithFields(logrus.Fields{
|
||||||
|
"alias": rawAlias,
|
||||||
|
"alias_key": aliasKey,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"nonce": nonce,
|
||||||
|
"body_len": len(body),
|
||||||
|
"body_sha256": hex.EncodeToString(bodyHash[:]),
|
||||||
|
"body_base64": base64.StdEncoding.EncodeToString(body),
|
||||||
|
"provided_hex_full": hex.EncodeToString(providedSig),
|
||||||
|
"expected_hex_full": hex.EncodeToString(expectedSignature),
|
||||||
|
}).Warn("sso sync signature mismatch")
|
||||||
|
return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.registerNonce(c.Context(), aliasKey, nonce); err != nil {
|
||||||
|
return "", config.SSOClientConfig{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliasKey, clientCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) verifyAuthorization(c *fiber.Ctx, alias string) error {
|
||||||
|
|
||||||
|
authHeader := strings.TrimSpace(c.Get(fiber.HeaderAuthorization))
|
||||||
|
if authHeader == "" {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "missing authorization header")
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
|
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.TrimSpace(parts[1])
|
||||||
|
if token == "" {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header")
|
||||||
|
}
|
||||||
|
|
||||||
|
verification, err := sso.VerifyAccessToken(token)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "invalid access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if verification.ServiceAlias == "" || verification.ServiceAlias != alias {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "service subject mismatch")
|
||||||
|
}
|
||||||
|
if !containsScope(verification.Claims.Scopes(), "sync.users") {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "missing sync scope")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) upsertUser(c *fiber.Ctx, alias string, req *userSyncRequest) error {
|
||||||
|
entity := &entity.User{
|
||||||
|
IdUser: req.User.ID,
|
||||||
|
Email: req.User.Email,
|
||||||
|
Name: req.User.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: MIGRATION TO UPSERT BASE REPOSITORY
|
||||||
|
if err := h.repo.UpsertByIdUser(c.Context(), entity); err != nil {
|
||||||
|
h.log.Errorf("sso user upsert failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to upsert user")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.repo.GetByIdUser(c.Context(), req.User.ID, nil)
|
||||||
|
if err != nil {
|
||||||
|
h.log.Errorf("sso user fetch after upsert failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to load user")
|
||||||
|
}
|
||||||
|
|
||||||
|
h.log.WithFields(logrus.Fields{
|
||||||
|
"action": req.Action,
|
||||||
|
"public_id": req.PublicID,
|
||||||
|
"alias": alias,
|
||||||
|
"user_id": req.User.ID,
|
||||||
|
}).Info("sso user synced")
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("User %s successfully", req.Action)
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: msg,
|
||||||
|
Data: dto.ToUserListDTO(*user),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) logoutUser(c *fiber.Ctx, alias string, req *userSyncRequest) error {
|
||||||
|
revoker := session.GetRevocationStore()
|
||||||
|
if revoker != nil {
|
||||||
|
if err := revoker.MarkUserLogout(c.Context(), uint(req.User.ID), time.Now().UTC()); err != nil {
|
||||||
|
h.log.WithError(err).Error("sso user logout revoke failed")
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to revoke user session")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h.log.Warn("sso user logout received but revocation store not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
h.log.WithFields(logrus.Fields{
|
||||||
|
"action": req.Action,
|
||||||
|
"public_id": req.PublicID,
|
||||||
|
"alias": alias,
|
||||||
|
"user_id": req.User.ID,
|
||||||
|
}).Info("sso user logout enforced")
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "User sessions revoked successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) removeUser(c *fiber.Ctx, alias string, req *userSyncRequest) error {
|
||||||
|
if err := h.repo.SoftDeleteByIdUser(c.Context(), req.User.ID); err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "user not found")
|
||||||
|
}
|
||||||
|
h.log.Errorf("sso user delete failed: %v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "failed to delete user")
|
||||||
|
}
|
||||||
|
|
||||||
|
h.log.WithFields(logrus.Fields{
|
||||||
|
"action": req.Action,
|
||||||
|
"public_id": req.PublicID,
|
||||||
|
"alias": alias,
|
||||||
|
"user_id": req.User.ID,
|
||||||
|
}).Info("sso user deleted")
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "User deleted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) registerNonce(ctx context.Context, alias, nonce string) error {
|
||||||
|
ttl := h.nonceTTL
|
||||||
|
if ttl <= 0 {
|
||||||
|
ttl = defaultNonceTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
key := fmt.Sprintf("sso:sync:%s:%s", alias, nonce)
|
||||||
|
if h.redis != nil {
|
||||||
|
stored, err := h.redis.SetNX(ctx, key, "1", ttl).Result()
|
||||||
|
if err == nil {
|
||||||
|
if !stored {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "nonce already used")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h.log.Errorf("store sync nonce failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if expRaw, ok := h.localNonces.Load(key); ok {
|
||||||
|
if expTime, ok := expRaw.(time.Time); ok && expTime.After(now) {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "nonce already used")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.localNonces.Store(key, now.Add(ttl))
|
||||||
|
h.pruneLocalNonces(now)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) calculateSignature(secret, alias, timestamp, nonce string, body []byte) []byte {
|
||||||
|
mac := hmac.New(sha256.New, []byte(secret))
|
||||||
|
mac.Write([]byte(alias))
|
||||||
|
mac.Write([]byte("\n"))
|
||||||
|
mac.Write([]byte(timestamp))
|
||||||
|
mac.Write([]byte("\n"))
|
||||||
|
mac.Write([]byte(nonce))
|
||||||
|
mac.Write([]byte("\n"))
|
||||||
|
mac.Write(body)
|
||||||
|
return mac.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsScope(scopes []string, target string) bool {
|
||||||
|
target = strings.ToLower(strings.TrimSpace(target))
|
||||||
|
if target == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, scope := range scopes {
|
||||||
|
if strings.ToLower(strings.TrimSpace(scope)) == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeSignature(sig string) ([]byte, error) {
|
||||||
|
sig = strings.TrimSpace(sig)
|
||||||
|
if sig == "" {
|
||||||
|
return nil, errors.New("empty signature")
|
||||||
|
}
|
||||||
|
if decoded, err := hex.DecodeString(sig); err == nil {
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(sig); err == nil {
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
if decoded, err := base64.URLEncoding.DecodeString(sig); err == nil {
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("unrecognized signature encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserSyncController) pruneLocalNonces(now time.Time) {
|
||||||
|
h.localNonces.Range(func(key, value any) bool {
|
||||||
|
exp, ok := value.(time.Time)
|
||||||
|
if !ok || exp.Before(now) {
|
||||||
|
h.localNonces.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct{}
|
||||||
|
|
||||||
|
func (Module) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
Routes(router, db, validate)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/cache"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
ssoController "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/controllers"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
|
userRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
ttl := config.SSOPKCETTL
|
||||||
|
if ttl <= 0 {
|
||||||
|
ttl = 5 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
store := session.NewStore(cache.MustRedis(), ttl)
|
||||||
|
ctrl := ssoController.NewController(&http.Client{Timeout: 10 * time.Second}, store, session.GetRevocationStore())
|
||||||
|
userRepo := userRepository.NewUserRepository(db)
|
||||||
|
syncCtrl := ssoController.NewUserSyncController(validate, userRepo, cache.Redis(), config.SSOClients)
|
||||||
|
|
||||||
|
group := router.Group("/sso")
|
||||||
|
group.Get("/start", middleware.NewLimiter(30, time.Minute), ctrl.Start)
|
||||||
|
group.Get("/callback", ctrl.Callback)
|
||||||
|
group.Get("/userinfo", middleware.NewLimiter(60, time.Minute), ctrl.UserInfo)
|
||||||
|
group.Post("/logout", middleware.NewLimiter(60, time.Minute), ctrl.Logout)
|
||||||
|
group.Post("/users/sync", middleware.NewLimiter(30, time.Minute), syncCtrl.Sync)
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RevocationStore handles token blacklist / revocation entries in Redis.
|
||||||
|
type RevocationStore struct {
|
||||||
|
redis *redis.Client
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalRevokerMu sync.RWMutex
|
||||||
|
globalRevoker *RevocationStore
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRevocationStore creates a revocation store with the given redis client and key prefix.
|
||||||
|
func NewRevocationStore(client *redis.Client, prefix string) *RevocationStore {
|
||||||
|
return &RevocationStore{
|
||||||
|
redis: client,
|
||||||
|
prefix: strings.TrimSpace(prefix),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRevocationStore registers the provided revocation store for global access.
|
||||||
|
func SetRevocationStore(store *RevocationStore) {
|
||||||
|
globalRevokerMu.Lock()
|
||||||
|
globalRevoker = store
|
||||||
|
globalRevokerMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRevocationStore returns the globally registered revocation store, or nil if unset.
|
||||||
|
func GetRevocationStore() *RevocationStore {
|
||||||
|
globalRevokerMu.RLock()
|
||||||
|
defer globalRevokerMu.RUnlock()
|
||||||
|
return globalRevoker
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustRevocationStore returns the registered revocation store or panics if none is configured.
|
||||||
|
func MustRevocationStore() *RevocationStore {
|
||||||
|
store := GetRevocationStore()
|
||||||
|
if store == nil {
|
||||||
|
panic("revocation store not initialised")
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke stores the fingerprint with the provided TTL.
|
||||||
|
func (s *RevocationStore) Revoke(ctx context.Context, fingerprint string, ttl time.Duration) error {
|
||||||
|
if s == nil || s.redis == nil {
|
||||||
|
return errors.New("revocation store redis client not initialised")
|
||||||
|
}
|
||||||
|
fingerprint = strings.TrimSpace(fingerprint)
|
||||||
|
if fingerprint == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if ttl <= 0 {
|
||||||
|
ttl = time.Minute
|
||||||
|
}
|
||||||
|
key := s.keyFor(fingerprint)
|
||||||
|
return s.redis.Set(ctx, key, "1", ttl).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRevoked returns true when the fingerprint appears in the blacklist.
|
||||||
|
func (s *RevocationStore) IsRevoked(ctx context.Context, fingerprint string) (bool, error) {
|
||||||
|
if s == nil || s.redis == nil {
|
||||||
|
return false, errors.New("revocation store redis client not initialised")
|
||||||
|
}
|
||||||
|
fingerprint = strings.TrimSpace(fingerprint)
|
||||||
|
if fingerprint == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
key := s.keyFor(fingerprint)
|
||||||
|
exists, err := s.redis.Exists(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return exists > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkUserLogout stores the timestamp of the last forced logout for the given user.
|
||||||
|
func (s *RevocationStore) MarkUserLogout(ctx context.Context, userID uint, at time.Time) error {
|
||||||
|
if s == nil || s.redis == nil {
|
||||||
|
return errors.New("revocation store redis client not initialised")
|
||||||
|
}
|
||||||
|
if userID == 0 {
|
||||||
|
return errors.New("invalid user id")
|
||||||
|
}
|
||||||
|
key := s.userLogoutKey(userID)
|
||||||
|
return s.redis.Set(ctx, key, at.UTC().Format(time.RFC3339Nano), 0).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearUserLogout removes any stored forced logout marker for the given user.
|
||||||
|
func (s *RevocationStore) ClearUserLogout(ctx context.Context, userID uint) error {
|
||||||
|
if s == nil || s.redis == nil {
|
||||||
|
return errors.New("revocation store redis client not initialised")
|
||||||
|
}
|
||||||
|
if userID == 0 {
|
||||||
|
return errors.New("invalid user id")
|
||||||
|
}
|
||||||
|
key := s.userLogoutKey(userID)
|
||||||
|
return s.redis.Del(ctx, key).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserLogoutTime returns the timestamp of the last forced logout for the given user.
|
||||||
|
func (s *RevocationStore) UserLogoutTime(ctx context.Context, userID uint) (time.Time, error) {
|
||||||
|
var zero time.Time
|
||||||
|
if s == nil || s.redis == nil {
|
||||||
|
return zero, errors.New("revocation store redis client not initialised")
|
||||||
|
}
|
||||||
|
if userID == 0 {
|
||||||
|
return zero, errors.New("invalid user id")
|
||||||
|
}
|
||||||
|
key := s.userLogoutKey(userID)
|
||||||
|
value, err := s.redis.Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, redis.Nil) {
|
||||||
|
return zero, nil
|
||||||
|
}
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
ts, err := time.Parse(time.RFC3339Nano, value)
|
||||||
|
if err != nil {
|
||||||
|
return zero, err
|
||||||
|
}
|
||||||
|
return ts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RevocationStore) keyFor(fingerprint string) string {
|
||||||
|
prefix := s.prefix
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = "sso:blacklist"
|
||||||
|
}
|
||||||
|
return prefix + ":" + fingerprint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RevocationStore) userLogoutKey(userID uint) string {
|
||||||
|
prefix := s.prefix
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = "sso:blacklist"
|
||||||
|
}
|
||||||
|
return prefix + ":user-logout:" + strconv.FormatUint(uint64(userID), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenFingerprint hashes token material before persisting it to the blacklist.
|
||||||
|
func TokenFingerprint(token string) string {
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256([]byte(token))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
const keyTemplate = "sso:pkce:%s"
|
||||||
|
|
||||||
|
// PKCESession holds data required to complete the OAuth2 PKCE exchange.
|
||||||
|
type PKCESession struct {
|
||||||
|
CodeVerifier string `json:"code_verifier"`
|
||||||
|
Nonce string `json:"nonce"`
|
||||||
|
ClientAlias string `json:"client_alias"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
RedirectURI string `json:"redirect_uri"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
ReturnTo string `json:"return_to,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store persists pkce sessions inside Redis using a configurable TTL.
|
||||||
|
type Store struct {
|
||||||
|
redis *redis.Client
|
||||||
|
ttl time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(client *redis.Client, ttl time.Duration) *Store {
|
||||||
|
return &Store{redis: client, ttl: ttl}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Save(ctx context.Context, state string, payload *PKCESession) error {
|
||||||
|
if s.redis == nil {
|
||||||
|
return fmt.Errorf("redis client is not initialised")
|
||||||
|
}
|
||||||
|
bytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.redis.Set(ctx, fmt.Sprintf(keyTemplate, state), bytes, s.ttl).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Get(ctx context.Context, state string) (*PKCESession, error) {
|
||||||
|
if s.redis == nil {
|
||||||
|
return nil, fmt.Errorf("redis client is not initialised")
|
||||||
|
}
|
||||||
|
raw, err := s.redis.Get(ctx, fmt.Sprintf(keyTemplate, state)).Result()
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var payload PKCESession
|
||||||
|
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Delete(ctx context.Context, state string) error {
|
||||||
|
if s.redis == nil {
|
||||||
|
return fmt.Errorf("redis client is not initialised")
|
||||||
|
}
|
||||||
|
return s.redis.Del(ctx, fmt.Sprintf(keyTemplate, state)).Err()
|
||||||
|
}
|
||||||
@@ -71,70 +71,70 @@ func (u *UserController) GetOne(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserController) CreateOne(c *fiber.Ctx) error {
|
// func (u *UserController) CreateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Create)
|
// req := new(validation.Create)
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
// if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
// }
|
||||||
|
|
||||||
result, err := u.UserService.CreateOne(c, req)
|
// result, err := u.UserService.CreateOne(c, req)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).
|
// return c.Status(fiber.StatusCreated).
|
||||||
JSON(response.Success{
|
// JSON(response.Success{
|
||||||
Code: fiber.StatusCreated,
|
// Code: fiber.StatusCreated,
|
||||||
Status: "success",
|
// Status: "success",
|
||||||
Message: "Create user successfully",
|
// Message: "Create user successfully",
|
||||||
Data: dto.ToUserListDTO(*result),
|
// Data: dto.ToUserListDTO(*result),
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (u *UserController) UpdateOne(c *fiber.Ctx) error {
|
// func (u *UserController) UpdateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Update)
|
// req := new(validation.Update)
|
||||||
param := c.Params("id")
|
// param := c.Params("id")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
// id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
// if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
// }
|
||||||
|
|
||||||
result, err := u.UserService.UpdateOne(c, req, uint(id))
|
// result, err := u.UserService.UpdateOne(c, req, uint(id))
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
// return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
// JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
// Code: fiber.StatusOK,
|
||||||
Status: "success",
|
// Status: "success",
|
||||||
Message: "Update user successfully",
|
// Message: "Update user successfully",
|
||||||
Data: dto.ToUserListDTO(*result),
|
// Data: dto.ToUserListDTO(*result),
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (u *UserController) DeleteOne(c *fiber.Ctx) error {
|
// func (u *UserController) DeleteOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
// param := c.Params("id")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
// id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if err := u.UserService.DeleteOne(c, uint(id)); err != nil {
|
// if err := u.UserService.DeleteOne(c, uint(id)); err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
// return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Common{
|
// JSON(response.Common{
|
||||||
Code: fiber.StatusOK,
|
// Code: fiber.StatusOK,
|
||||||
Status: "success",
|
// Status: "success",
|
||||||
Message: "Delete user successfully",
|
// Message: "Delete user successfully",
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,21 +1,112 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgconn"
|
||||||
|
commonrepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
repository.BaseRepository[entity.User]
|
commonrepo.BaseRepository[entity.User]
|
||||||
|
GetByIdUser(ctx context.Context, idUser int64, modifier func(*gorm.DB) *gorm.DB) (*entity.User, error)
|
||||||
|
UpsertByIdUser(ctx context.Context, user *entity.User) error
|
||||||
|
SoftDeleteByIdUser(ctx context.Context, idUser int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRepositoryImpl struct {
|
type UserRepositoryImpl struct {
|
||||||
*repository.BaseRepositoryImpl[entity.User]
|
*commonrepo.BaseRepositoryImpl[entity.User]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||||
return &UserRepositoryImpl{
|
return &UserRepositoryImpl{
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.User](db),
|
BaseRepositoryImpl: commonrepo.NewBaseRepository[entity.User](db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UserRepositoryImpl) GetByIdUser(
|
||||||
|
ctx context.Context,
|
||||||
|
idUser int64,
|
||||||
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
|
) (*entity.User, error) {
|
||||||
|
return r.BaseRepositoryImpl.First(ctx, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("id_user = ?", idUser)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepositoryImpl) UpsertByIdUser(ctx context.Context, user *entity.User) error {
|
||||||
|
if user == nil {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
now := time.Now()
|
||||||
|
user.DeletedAt = gorm.DeletedAt{}
|
||||||
|
user.UpdatedAt = now
|
||||||
|
|
||||||
|
err := tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id_user"}},
|
||||||
|
UpdateAll: true,
|
||||||
|
}).Omit("id", "created_at").Create(user).Error
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isUniqueViolation(err, "users_email_unique") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing entity.User
|
||||||
|
lockQuery := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("email = ?", user.Email)
|
||||||
|
if err := lockQuery.First(&existing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Id = existing.Id
|
||||||
|
|
||||||
|
updates := map[string]any{
|
||||||
|
"id_user": user.IdUser,
|
||||||
|
"email": user.Email,
|
||||||
|
"name": user.Name,
|
||||||
|
"updated_at": now,
|
||||||
|
"deleted_at": gorm.DeletedAt{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(&entity.User{}).Where("id = ?", existing.Id).Updates(updates).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepositoryImpl) SoftDeleteByIdUser(ctx context.Context, idUser int64) error {
|
||||||
|
query := r.DB().WithContext(ctx).Where("id_user = ?", idUser)
|
||||||
|
result := query.Delete(&entity.User{})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUniqueViolation(err error, constraint string) bool {
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
if !errors.As(err, &pgErr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if pgErr.Code != "23505" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if constraint == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return pgErr.ConstraintName == constraint
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
package users
|
package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/users/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/users/controllers"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func UserRoutes(v1 fiber.Router, s user.UserService) {
|
func UserRoutes(v1 fiber.Router, s user.UserService) {
|
||||||
ctrl := controller.NewUserController(s)
|
ctrl := controller.NewUserController(s)
|
||||||
|
|
||||||
route := v1.Group("/users")
|
route := v1.Group("/users")
|
||||||
|
route.Use(middleware.Auth(s))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
// route.Post("/", ctrl.CreateOne)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
// route.Patch("/:id", ctrl.UpdateOne)
|
||||||
route.Delete("/:id", ctrl.DeleteOne)
|
// route.Delete("/:id", ctrl.DeleteOne)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type UserService interface {
|
|||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.User, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.User, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.User, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.User, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
GetBySSOUserID(ctx *fiber.Ctx, ssoUserID uint) (*entity.User, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type userService struct {
|
type userService struct {
|
||||||
@@ -68,6 +69,18 @@ func (s userService) GetOne(c *fiber.Ctx, id uint) (*entity.User, error) {
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s userService) GetBySSOUserID(c *fiber.Ctx, ssoUserID uint) (*entity.User, error) {
|
||||||
|
user, err := s.Repository.GetByIdUser(c.Context(), int64(ssoUserID), nil)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get user by SSO id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.User, error) {
|
func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.User, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals"
|
||||||
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
|
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
|
||||||
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
|
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
|
||||||
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
||||||
|
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
||||||
|
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
|
||||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||||
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
|
||||||
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals"
|
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,8 +29,9 @@ func Routes(app *fiber.App, db *gorm.DB) {
|
|||||||
master.MasterModule{},
|
master.MasterModule{},
|
||||||
constants.ConstantModule{},
|
constants.ConstantModule{},
|
||||||
inventory.InventoryModule{},
|
inventory.InventoryModule{},
|
||||||
production.ProductionModule{},
|
production.ProductionModule{},
|
||||||
approvals.ApprovalModule{},
|
approvals.ApprovalModule{},
|
||||||
|
ssoModule.Module{},
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/cache"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
profileCachePrefix = "sso:profile:user:"
|
||||||
|
profileCacheTTL = time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
profileClient = &http.Client{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
|
profileLocalCache sync.Map // map[string]cachedProfile
|
||||||
|
)
|
||||||
|
|
||||||
|
type cachedProfile struct {
|
||||||
|
Profile *UserProfile
|
||||||
|
ExpiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserProfile represents the enriched user information returned by the central SSO.
|
||||||
|
type UserProfile struct {
|
||||||
|
UserID uint
|
||||||
|
Roles []Role
|
||||||
|
Permissions []Permission
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role describes a role assignment from the SSO profile response.
|
||||||
|
type Role struct {
|
||||||
|
ID uint
|
||||||
|
Key string
|
||||||
|
Name string
|
||||||
|
ClientID uint
|
||||||
|
ClientAlias string
|
||||||
|
ClientName string
|
||||||
|
Permissions []Permission
|
||||||
|
RawReference json.RawMessage `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permission describes a granular permission entry from the SSO profile.
|
||||||
|
type Permission struct {
|
||||||
|
ID uint
|
||||||
|
Name string
|
||||||
|
Action string
|
||||||
|
ClientID uint
|
||||||
|
ClientAlias string
|
||||||
|
ClientName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermissionNames returns a de-duplicated slice of permission identifiers in canonical form.
|
||||||
|
func (p *UserProfile) PermissionNames() []string {
|
||||||
|
if p == nil || len(p.Permissions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
set := make(map[string]struct{}, len(p.Permissions))
|
||||||
|
for _, perm := range p.Permissions {
|
||||||
|
name := canonicalPermissionName(perm.Name)
|
||||||
|
if name != "" {
|
||||||
|
set[name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := make([]string, 0, len(set))
|
||||||
|
for name := range set {
|
||||||
|
out = append(out, name)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchProfile retrieves the SSO profile for the authenticated user, using Redis/in-memory
|
||||||
|
// caching to reduce load on the SSO service. Only end-user tokens (subject user:ID) are supported.
|
||||||
|
func FetchProfile(ctx context.Context, token string, verification *VerificationResult) (*UserProfile, error) {
|
||||||
|
if verification == nil || verification.UserID == 0 {
|
||||||
|
return nil, errors.New("profile only available for user tokens")
|
||||||
|
}
|
||||||
|
key := profileCacheKey(verification.UserID)
|
||||||
|
|
||||||
|
if profile := loadProfileFromLocalCache(key); profile != nil {
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile := loadProfileFromRedis(ctx, key); profile != nil {
|
||||||
|
storeProfileInLocalCache(key, profile)
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := fetchProfileFromSSO(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
storeProfileInLocalCache(key, profile)
|
||||||
|
storeProfileInRedis(ctx, key, profile)
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error) {
|
||||||
|
endpoint := strings.TrimSpace(config.SSOGetMeURL)
|
||||||
|
if endpoint == "" {
|
||||||
|
return nil, errors.New("sso get-me endpoint not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build profile request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
if cookieName := strings.TrimSpace(config.SSOAccessCookieName); cookieName != "" {
|
||||||
|
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", cookieName, token))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := profileClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetch profile: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf("fetch profile: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var envelope userInfoEnvelope
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roles := envelope.getRoles()
|
||||||
|
profile := &UserProfile{}
|
||||||
|
|
||||||
|
// Attempt to infer user id if provided.
|
||||||
|
if envelope.User != nil && envelope.User.ID > 0 {
|
||||||
|
profile.UserID = uint(envelope.User.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
perms := make([]Permission, 0)
|
||||||
|
convertedRoles := make([]Role, 0, len(roles))
|
||||||
|
for _, r := range roles {
|
||||||
|
role := Role{
|
||||||
|
ID: uint(r.ID),
|
||||||
|
Key: strings.TrimSpace(r.Key),
|
||||||
|
Name: strings.TrimSpace(r.Name),
|
||||||
|
ClientAlias: strings.TrimSpace(r.Client.Alias),
|
||||||
|
ClientName: strings.TrimSpace(r.Client.Name),
|
||||||
|
ClientID: uint(r.Client.ID),
|
||||||
|
}
|
||||||
|
rolePerms := make([]Permission, 0, len(r.Permissions))
|
||||||
|
for _, p := range r.Permissions {
|
||||||
|
perm := Permission{
|
||||||
|
ID: uint(p.ID),
|
||||||
|
Name: strings.TrimSpace(p.Name),
|
||||||
|
Action: strings.TrimSpace(p.Action),
|
||||||
|
ClientAlias: strings.TrimSpace(p.Client.Alias),
|
||||||
|
ClientName: strings.TrimSpace(p.Client.Name),
|
||||||
|
ClientID: uint(p.Client.ID),
|
||||||
|
}
|
||||||
|
if perm.Name != "" {
|
||||||
|
rolePerms = append(rolePerms, perm)
|
||||||
|
perms = append(perms, perm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
role.Permissions = rolePerms
|
||||||
|
convertedRoles = append(convertedRoles, role)
|
||||||
|
}
|
||||||
|
profile.Roles = convertedRoles
|
||||||
|
profile.Permissions = perms
|
||||||
|
|
||||||
|
return profile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadProfileFromLocalCache(key string) *UserProfile {
|
||||||
|
if value, ok := profileLocalCache.Load(key); ok {
|
||||||
|
if cached, ok := value.(cachedProfile); ok {
|
||||||
|
if time.Now().Before(cached.ExpiresAt) && cached.Profile != nil {
|
||||||
|
return cached.Profile
|
||||||
|
}
|
||||||
|
profileLocalCache.Delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadProfileFromRedis(ctx context.Context, key string) *UserProfile {
|
||||||
|
client := cache.Redis()
|
||||||
|
if client == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := client.Get(ctx, key).Bytes()
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, redis.Nil) {
|
||||||
|
utils.Log.WithError(err).Warn("sso profile redis lookup failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile UserProfile
|
||||||
|
if err := json.Unmarshal(data, &profile); err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("sso profile redis decode failed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &profile
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeProfileInLocalCache(key string, profile *UserProfile) {
|
||||||
|
if profile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profileLocalCache.Store(key, cachedProfile{
|
||||||
|
Profile: profile,
|
||||||
|
ExpiresAt: time.Now().Add(profileCacheTTL),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeProfileInRedis(ctx context.Context, key string, profile *UserProfile) {
|
||||||
|
client := cache.Redis()
|
||||||
|
if client == nil || profile == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(profile)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("sso profile redis encode failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Set(ctx, key, data, profileCacheTTL).Err(); err != nil {
|
||||||
|
utils.Log.WithError(err).Warn("sso profile redis store failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileCacheKey(userID uint) string {
|
||||||
|
return profileCachePrefix + strconv.FormatUint(uint64(userID), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermissionName(name string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// userInfoEnvelope handles the varying shapes returned by the SSO userinfo endpoint.
|
||||||
|
type userInfoEnvelope struct {
|
||||||
|
Roles []userInfoRole `json:"roles"`
|
||||||
|
Data *struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Roles []userInfoRole `json:"roles"`
|
||||||
|
} `json:"data"`
|
||||||
|
User *struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
} `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *userInfoEnvelope) getRoles() []userInfoRole {
|
||||||
|
if len(e.Roles) > 0 {
|
||||||
|
return e.Roles
|
||||||
|
}
|
||||||
|
if e.Data != nil && len(e.Data.Roles) > 0 {
|
||||||
|
if e.User == nil && e.Data.ID > 0 {
|
||||||
|
e.User = &struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}{ID: e.Data.ID}
|
||||||
|
}
|
||||||
|
return e.Data.Roles
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type userInfoRole struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Client userInfoClient `json:"client"`
|
||||||
|
Permissions []userInfoPermRaw `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userInfoClient struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userInfoPermRaw struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Client userInfoClient `json:"client"`
|
||||||
|
Details any `json:"details"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package sso
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/MicahParks/keyfunc/v2"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type verifier struct {
|
||||||
|
jwks *keyfunc.JWKS
|
||||||
|
issuer string
|
||||||
|
audiences map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccessTokenClaims struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c AccessTokenClaims) Scopes() []string {
|
||||||
|
if c.Scope == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return strings.Fields(c.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerificationResult struct {
|
||||||
|
UserID uint
|
||||||
|
ServiceAlias string
|
||||||
|
Subject string
|
||||||
|
Claims *AccessTokenClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalMu sync.RWMutex
|
||||||
|
globalV *verifier
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(ctx context.Context, jwksURL, issuer string, audiences []string) error {
|
||||||
|
jwksURL = strings.TrimSpace(jwksURL)
|
||||||
|
issuer = strings.TrimSpace(issuer)
|
||||||
|
if jwksURL == "" || issuer == "" {
|
||||||
|
return errors.New("missing SSO JWKS or issuer configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
options := keyfunc.Options{
|
||||||
|
Ctx: ctx,
|
||||||
|
Client: client,
|
||||||
|
RefreshTimeout: 10 * time.Second,
|
||||||
|
RefreshInterval: time.Hour,
|
||||||
|
RefreshUnknownKID: true,
|
||||||
|
RefreshErrorHandler: func(err error) {
|
||||||
|
utils.Log.Errorf("sso jwks refresh failed: %v", err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jwks, err := keyfunc.Get(jwksURL, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load jwks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
audienceMap := make(map[string]struct{}, len(audiences))
|
||||||
|
for _, aud := range audiences {
|
||||||
|
aud = strings.TrimSpace(aud)
|
||||||
|
if aud == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
audienceMap[aud] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
globalMu.Lock()
|
||||||
|
globalV = &verifier{jwks: jwks, issuer: issuer, audiences: audienceMap}
|
||||||
|
globalMu.Unlock()
|
||||||
|
|
||||||
|
utils.Log.Infof("sso verifier initialized for issuer %s (%d keys)", issuer, len(jwks.KIDs()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyAccessToken(token string) (*VerificationResult, error) {
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" {
|
||||||
|
return nil, errors.New("empty token")
|
||||||
|
}
|
||||||
|
|
||||||
|
globalMu.RLock()
|
||||||
|
v := globalV
|
||||||
|
globalMu.RUnlock()
|
||||||
|
if v == nil {
|
||||||
|
return nil, errors.New("sso verifier not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := &AccessTokenClaims{}
|
||||||
|
parser := jwt.NewParser(
|
||||||
|
jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
|
||||||
|
jwt.WithIssuedAt(),
|
||||||
|
jwt.WithExpirationRequired(),
|
||||||
|
)
|
||||||
|
|
||||||
|
tok, err := parser.ParseWithClaims(token, claims, v.jwks.Keyfunc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse token: %w", err)
|
||||||
|
}
|
||||||
|
if !tok.Valid {
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Issuer != v.issuer {
|
||||||
|
return nil, errors.New("unexpected token issuer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.audiences) > 0 {
|
||||||
|
validAud := false
|
||||||
|
for _, aud := range claims.Audience {
|
||||||
|
if _, ok := v.audiences[aud]; ok {
|
||||||
|
validAud = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !validAud {
|
||||||
|
return nil, errors.New("unexpected token audience")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := strings.TrimSpace(claims.Subject)
|
||||||
|
if sub == "" {
|
||||||
|
return nil, errors.New("missing subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &VerificationResult{Claims: claims, Subject: sub}
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(sub, "user:"):
|
||||||
|
idStr := strings.TrimPrefix(sub, "user:")
|
||||||
|
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid subject: %w", err)
|
||||||
|
}
|
||||||
|
result.UserID = uint(id)
|
||||||
|
case strings.HasPrefix(sub, "service:"):
|
||||||
|
alias := strings.TrimSpace(strings.TrimPrefix(sub, "service:"))
|
||||||
|
if alias == "" {
|
||||||
|
return nil, errors.New("invalid service subject")
|
||||||
|
}
|
||||||
|
result.ServiceAlias = strings.ToLower(alias)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported subject type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package secure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pkceCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
||||||
|
|
||||||
|
// RandomBytes returns securely generated random bytes of given length.
|
||||||
|
func RandomBytes(length int) ([]byte, error) {
|
||||||
|
if length <= 0 {
|
||||||
|
return nil, fmt.Errorf("length must be positive")
|
||||||
|
}
|
||||||
|
b := make([]byte, length)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomString returns a base64url encoded random string of approximately the requested length.
|
||||||
|
func RandomString(length int) (string, error) {
|
||||||
|
if length <= 0 {
|
||||||
|
return "", fmt.Errorf("length must be positive")
|
||||||
|
}
|
||||||
|
// Generate ceil(length * 6/8) bytes to have enough entropy for base64 url encoding.
|
||||||
|
byteLen := (length*6 + 7) / 8
|
||||||
|
bytes, err := RandomBytes(byteLen)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
s := base64.RawURLEncoding.EncodeToString(bytes)
|
||||||
|
if len(s) > length {
|
||||||
|
return s[:length], nil
|
||||||
|
}
|
||||||
|
// If encoded string shorter, pad using charset
|
||||||
|
if len(s) < length {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.WriteString(s)
|
||||||
|
extraNeeded := length - len(s)
|
||||||
|
more, err := randomFromCharset(extraNeeded)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
sb.WriteString(more)
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCECodeVerifier generates a random string compliant with RFC 7636.
|
||||||
|
func PKCECodeVerifier(length int) (string, error) {
|
||||||
|
if length < 43 {
|
||||||
|
length = 43
|
||||||
|
}
|
||||||
|
if length > 128 {
|
||||||
|
length = 128
|
||||||
|
}
|
||||||
|
return randomFromCharset(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomFromCharset returns a random string of given length using pkceCharset.
|
||||||
|
func randomFromCharset(length int) (string, error) {
|
||||||
|
if length <= 0 {
|
||||||
|
return "", fmt.Errorf("length must be positive")
|
||||||
|
}
|
||||||
|
bytes, err := RandomBytes(length)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
out := make([]byte, length)
|
||||||
|
for i, b := range bytes {
|
||||||
|
out[i] = pkceCharset[int(b)%len(pkceCharset)]
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64URLEncode encodes the input bytes using base64 URL encoding without padding.
|
||||||
|
func Base64URLEncode(data []byte) string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString(data)
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
func VerifyToken(tokenStr, secret, tokenType string) (uint, error) {
|
|
||||||
token, err := jwt.Parse(tokenStr, func(_ *jwt.Token) (interface{}, error) {
|
|
||||||
return []byte(secret), nil
|
|
||||||
})
|
|
||||||
if err != nil || !token.Valid {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := token.Claims.(jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
return 0, errors.New("invalid token claims")
|
|
||||||
}
|
|
||||||
|
|
||||||
jwtType, ok := claims["type"].(string)
|
|
||||||
if !ok || jwtType != tokenType {
|
|
||||||
return 0, errors.New("invalid token type")
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, ok := claims["sub"]
|
|
||||||
if !ok {
|
|
||||||
return 0, errors.New("invalid token sub")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := sub.(type) {
|
|
||||||
case float64:
|
|
||||||
return uint(v), nil
|
|
||||||
case string:
|
|
||||||
id, err := strconv.Atoi(v)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.New("invalid sub format")
|
|
||||||
}
|
|
||||||
return uint(id), nil
|
|
||||||
default:
|
|
||||||
return 0, errors.New("unsupported sub type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔍 Waiting for PostgreSQL at $DB_HOST:$DB_PORT..."
|
||||||
|
until nc -z "$DB_HOST" "$DB_PORT"; do
|
||||||
|
echo "⏳ PostgreSQL is not ready yet..."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "✅ PostgreSQL is ready!"
|
||||||
|
|
||||||
|
echo "🏁 Starting LTI API (with Air hot reload)..."
|
||||||
|
exec air -c .air.toml
|
||||||
Reference in New Issue
Block a user