Compare commits
435 Commits
cb91a10192
...
group-role
| Author | SHA1 | Date | |
|---|---|---|---|
| d35e5813b5 | |||
| 533e6ea6af | |||
| 0c24ed9382 | |||
| f5c61bb6a0 | |||
| eb05f830fe | |||
| d86a9de388 | |||
| d80caac81b | |||
| 5d49a661ed | |||
| 635a2d6058 | |||
| 1eb96e906d | |||
| 58974d9789 | |||
| 329fac415f | |||
| a83ec61fae | |||
| d5e86acacf | |||
| 221ef192bc | |||
| 18664dbd8b | |||
| d097735965 | |||
| 992776b8a6 | |||
| 3f260b9029 | |||
| 65f40d0897 | |||
| 6d482c784f | |||
| dc07868d15 | |||
| 09a2f05ee5 | |||
| 5cec1cf561 | |||
| 3281764eff | |||
| 868337134d | |||
| 9372673bf1 | |||
| 0eea81b42f | |||
| 7468303e41 | |||
| 8745e7d8bc | |||
| bdc42beb27 | |||
| 72083fa0a4 | |||
| b3ad13e55d | |||
| 0db54e0268 | |||
| b3ef96a0ce | |||
| a773f1f8b4 | |||
| 1a71f50914 | |||
| d17e154e42 | |||
| bad26775eb | |||
| c3fd6637a5 | |||
| 20173ea140 | |||
| 41d439beab | |||
| b36b6e18ca | |||
| 1765485027 | |||
| c5fb5e674a | |||
| 03bf655051 | |||
| f8589fce5d | |||
| ac62158de9 | |||
| 44e1a18e9a | |||
| e0f2c3219f | |||
| d2d52dc041 | |||
| d7d142152c | |||
| 5c321311cd | |||
| ffc8a5f44d | |||
| dbff94e7b3 | |||
| 0b1ef77689 | |||
| b0005c6702 | |||
| c2abf1a5ba | |||
| ac50929e6e | |||
| 32785398ca | |||
| cc497b6016 | |||
| 97ffcbdaa4 | |||
| d09bf8ff02 | |||
| c0814093e5 | |||
| d48519741d | |||
| 213991126d | |||
| c7e88606e3 | |||
| a0d506fb76 | |||
| 0ec7743fca | |||
| a8a0fa55b7 | |||
| 7321448ce7 | |||
| 6d5e0fc9a9 | |||
| ef05d66787 | |||
| b3296c45ad | |||
| 7fd163f957 | |||
| 0f0d50a684 | |||
| 68074e02bc | |||
| 8d38a86f86 | |||
| e0c095c24d | |||
| 4c318b15cd | |||
| 5ea6bc4251 | |||
| 1cbe908489 | |||
| 53ee156e67 | |||
| 07a936acc7 | |||
| f892f0da24 | |||
| 38955ee4e6 | |||
| 7fa7e87e88 | |||
| f085f2e271 | |||
| 08add259a4 | |||
| 5b6142dfa6 | |||
| dc41521a99 | |||
| 299e7eddc4 | |||
| b4699e987c | |||
| be9d4f2a1b | |||
| db99236501 | |||
| e33fb04c99 | |||
| c3d4208e12 | |||
| 3f945fa329 | |||
| 93a5cd7c70 | |||
| 951de989af | |||
| c5cf253a15 | |||
| d70032e36d | |||
| 445ac50537 | |||
| c13e564b01 | |||
| 5d3a77133d | |||
| 44592ebc08 | |||
| 1b941cb0c3 | |||
| 1cb520c2b6 | |||
| b3fdd3bc18 | |||
| 9110db2f08 | |||
| 5972735102 | |||
| 14c69349cc | |||
| 3ceeab04e1 | |||
| b7a67c208f | |||
| cee885a84d | |||
| 4b496ea9bd | |||
| 570ae6ac8c | |||
| f0d3a61e7b | |||
| b09567620f | |||
| 2209846525 | |||
| 108ed61961 | |||
| b73bfd590b | |||
| 19d56159ba | |||
| b9ccf6adac | |||
| 9a0870dbbc | |||
| 70429f69a2 | |||
| ad635008eb | |||
| eacc8fdd89 | |||
| d309fb3f57 | |||
| f4fd993679 | |||
| 016879b53f | |||
| 70bba15cda | |||
| 57daf175ab | |||
| 0817a65272 | |||
| 13f9da1a67 | |||
| 83535acf1c | |||
| 441ce2daca | |||
| f9848d2110 | |||
| 7f0511b0d4 | |||
| a27f2ad593 | |||
| 715a984241 | |||
| 66e1756ade | |||
| 849403a137 | |||
| 8d15c9b8b2 | |||
| 87af1834cf | |||
| 357583f54d | |||
| aa6de76ded | |||
| ab3c2d1eb0 | |||
| 644cf2a358 | |||
| 5b1ed9925d | |||
| 4071a50a37 | |||
| dd7c51efd8 | |||
| 8902f4d187 | |||
| 6666b20464 | |||
| c5f288ba1e | |||
| cc49ab1655 | |||
| 06c60b3491 | |||
| b584a7b07f | |||
| 410e420a46 | |||
| eeb0f6eac1 | |||
| fb622f918a | |||
| a50bad417f | |||
| c395729446 | |||
| eaa92d2fe4 | |||
| a9e382d713 | |||
| 974244025e | |||
| ae41076673 | |||
| cc7f7f40c4 | |||
| e2ae03f2a6 | |||
| 9319564dea | |||
| 83e3e5a2e9 | |||
| 2b40e4e922 | |||
| ed33d03fda | |||
| 34c152a459 | |||
| ad09e98bba | |||
| d3fd5cba16 | |||
| 64dbb4368c | |||
| cb3a6ddc58 | |||
| e774f415d8 | |||
| d5a22895e7 | |||
| 9983c51e3a | |||
| 6a1fc193f4 | |||
| 118877f727 | |||
| 7b8fe6baf2 | |||
| e85b23b3e8 | |||
| 6164b77bee | |||
| 8b5a5744ab | |||
| d9e9c5ab38 | |||
| 0dcef81b59 | |||
| 426b70a1de | |||
| 912973cdb5 | |||
| e4ff799f05 | |||
| 320715f5aa | |||
| a3b04b6243 | |||
| f610d7480f | |||
| 11ac92a026 | |||
| 98ae3e06e9 | |||
| a1146ce371 | |||
| a67ec7e78c | |||
| 9895392b50 | |||
| c6998f33e1 | |||
| 81659181e4 | |||
| 849b5935c2 | |||
| c27d837ab0 | |||
| 92e9b87227 | |||
| 243b7cce33 | |||
| 76d960619f | |||
| 3e59c78287 | |||
| 6cd9da69ab | |||
| 29b97a87b3 | |||
| 8bc4603274 | |||
| cc60a1ba86 | |||
| 89c7dc43e5 | |||
| 95c330568d | |||
| 900d314a95 | |||
| 0d8a3b1b39 | |||
| 4b7396c210 | |||
| d4e2cbdd4f | |||
| 5024ac8151 | |||
| 3bf08c5933 | |||
| b42da50306 | |||
| 0efc90567b | |||
| a5466f1b10 | |||
| 8e946cbee5 | |||
| a3a6b5e4d7 | |||
| ad0a0f5626 | |||
| 2389058ddc | |||
| ce44ef3e62 | |||
| 9ee30d1e23 | |||
| 886d0a7f5c | |||
| cfdf419460 | |||
| 930e069aee | |||
| 1ef261660f | |||
| 5d9e5d27bf | |||
| 3f8a4024ce | |||
| e8a74999c3 | |||
| dc2ce1f349 | |||
| bce775f692 | |||
| 45bce711f2 | |||
| 8ab2ddbe8e | |||
| 7f9b719b2b | |||
| 2b7f4995ef | |||
| 321f4087e1 | |||
| 9d19b470bc | |||
| 68493be36e | |||
| f8772f8de2 | |||
| d451331c66 | |||
| 485cfc2d12 | |||
| 944c650ab3 | |||
| dfc5587608 | |||
| 96bdbfda95 | |||
| 05a234b7a5 | |||
| 2f58c01c24 | |||
| e92dde20ca | |||
| 63437d6dc7 | |||
| cef9dae4d3 | |||
| 0d7b1355d5 | |||
| a213ea85d0 | |||
| 5c43f6d72a | |||
| e49c0bbe45 | |||
| 413a11ee63 | |||
| 4a112318bd | |||
| 9897eb1f5d | |||
| 3fc7ceac23 | |||
| aa48c21466 | |||
| 0ca2bb3f89 | |||
| cd5adcdc3f | |||
| 62c90d0597 | |||
| 665d12a828 | |||
| 4d455fd62e | |||
| 348aacfde2 | |||
| de17870bdb | |||
| 54581742dc | |||
| 639575dae0 | |||
| 800e1afbe5 | |||
| 8abc4396ac | |||
| 70f860824c | |||
| 3c5e31cbb2 | |||
| 3923b428a4 | |||
| 5b816c6873 | |||
| 66edadfeda | |||
| b872722e07 | |||
| 091218b42d | |||
| 03697b2f67 | |||
| 8a28fca3d9 | |||
| 1ab4113040 | |||
| 013f300513 | |||
| 45e31b41ca | |||
| 182f30f1ba | |||
| 7c97ebd84f | |||
| 9fefe3ac71 | |||
| ca3006c428 | |||
| 51b7e6b3f9 | |||
| db2cb36f54 | |||
| 78e84567c7 | |||
| 0423b3803f | |||
| 60e317b9e4 | |||
| aa18b9f3e2 | |||
| d9ca1ce2b4 | |||
| 41c3dfdfe4 | |||
| 725cc74102 | |||
|
83c26bb94a
|
|||
|
6be3aa07a1
|
|||
|
54021c3021
|
|||
| 4b3a814d7e | |||
| dd5c59afa8 | |||
| 0723a48ab0 | |||
| ffefee930a | |||
| a7ddd3d1ff | |||
| 21cedeabbd | |||
| 807d7538a0 | |||
| 8364a8e9ec | |||
| 56755ac531 | |||
| e4d83e75a0 | |||
| 3dd91cf238 | |||
| 03d6730151 | |||
| aa152a4127 | |||
| a1ed1113d9 | |||
| 2187c873ee | |||
| 595015f324 | |||
| 8504f9c230 | |||
| 04db9b8ef2 | |||
| e983719601 | |||
| c5c55f72b1 | |||
| c445756296 | |||
| 0166e62e98 | |||
| 8e22a3ac05 | |||
| 05ee30f6db | |||
| dd8c453c54 | |||
| 52870cb541 | |||
| 14b37c2220 | |||
| 5604a824fe | |||
| 7d0ddd4d77 | |||
| 07b9b94143 | |||
| b95dcc6230 | |||
| e8bad71f21 | |||
| 4df7561dd3 | |||
| 8f753b2561 | |||
| 11748bb68e | |||
| 491c9a824d | |||
| 476b9a13d9 | |||
| 42665fffbb | |||
| a157a3ec0e | |||
| 024d07fdd6 | |||
| 23845e25dd | |||
| 47209c311c | |||
| 2caef38ce6 | |||
| e88980e64f | |||
| 159e4ad0e2 | |||
| 6e2d67ad24 | |||
| d46e296ce1 | |||
| e98806e96f | |||
| 0ab82e2503 | |||
| 34c1ce7652 | |||
| 428dc50aa1 | |||
| 2663264f50 | |||
| ffba961d72 | |||
| 5a939c0771 | |||
| 87916f96fd | |||
| c6c03e9cb6 | |||
| 6fd7171450 | |||
| ae07d2d3d9 | |||
| 65545a0d71 | |||
| d423d9ba62 | |||
| d64c8479f8 | |||
| 3279f1fb90 | |||
| 1819629008 | |||
| 4e9fa2337b | |||
| 68899e98bd | |||
| 47f5188961 | |||
| 1840194bae | |||
| d3bcc785a1 | |||
| 64faa4ca5f | |||
| 24c72800ad | |||
| f559f54683 | |||
| 9b0de4512b | |||
| b6d365cc48 | |||
| 8f755b6d1e | |||
| 587a463623 | |||
| 1a596eef87 | |||
| e6b87a6561 | |||
| 3bcc5f8900 | |||
| c5ee912408 | |||
| 9766da7cfd | |||
| a004a82272 | |||
| 64ca9b922e | |||
| 38a2ce1ce9 | |||
| accde2662f | |||
| 92fda8cb24 | |||
| eee3839dea | |||
| b941561ccf | |||
| 0bda5495c4 | |||
| b5f5346536 | |||
| 68e2ece877 | |||
| 04bd27607f | |||
| eb42b61b2c | |||
| 06e0e90677 | |||
| eaf3596580 | |||
| b8f3fa0a32 | |||
| d4adc1b538 | |||
| edfa3e63b9 | |||
| 7c58473ff1 | |||
| 9473c83679 | |||
| 0b8c03e8c5 | |||
| 55eb4c9862 | |||
| de28470432 | |||
| 8ccf9f281c | |||
| a9df6fa559 | |||
| eb9c2b1da1 | |||
| af8b347173 | |||
| ba89880f8a | |||
| 07e1cbc66f | |||
| aee3306c2b | |||
| d2cb426170 | |||
| e9e1414c90 | |||
| 4a71f6c5ee | |||
| a8e75d75f0 | |||
| afc9208269 | |||
| ac07b5d723 | |||
| 9267cf2618 | |||
| 55ccd8ea8e | |||
| fdf99d82e5 | |||
| d9c3223228 | |||
| 3f369de3fa | |||
| 3cfc89eb39 | |||
| 6ea585cf23 | |||
|
eba6253ff6
|
|||
|
ef7e1a80d9
|
|||
|
854e1b44a9
|
|||
|
607174110c
|
|||
|
c283998403
|
|||
|
986ca8e353
|
|||
|
20d9947642
|
|||
|
97e15e1b1e
|
|||
|
d50bd6c4f5
|
63
.air.toml
Normal file
63
.air.toml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "bin\\hspguard"
|
||||||
|
cmd = "make build"
|
||||||
|
delay = 1000
|
||||||
|
exclude_dir = [
|
||||||
|
"assets",
|
||||||
|
"tmp",
|
||||||
|
"vendor",
|
||||||
|
"testdata",
|
||||||
|
"dist",
|
||||||
|
"migrations",
|
||||||
|
"queries",
|
||||||
|
"scripts",
|
||||||
|
"templates",
|
||||||
|
"web",
|
||||||
|
]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = []
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
silent = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
26
.env.example
26
.env.example
@@ -1,16 +1,24 @@
|
|||||||
|
|
||||||
PORT=3001
|
GUARD_PORT=3001
|
||||||
DATABASE_URL="postgres://<user>:<user>@<host>:<port>/<db>?sslmode=disable"
|
GUARD_HOST="127.0.0.1"
|
||||||
|
GUARD_URI="http://localhost:3001"
|
||||||
|
|
||||||
ADMIN_NAME="admin"
|
GUARD_DB_URL="postgres://<user>:<user>@<host>:<port>/<db>?sslmode=disable"
|
||||||
ADMIN_EMAIL="admin@test.net"
|
|
||||||
ADMIN_PASSWORD="secret"
|
|
||||||
|
|
||||||
JWT_PRIVATE_KEY="ecdsa"
|
GUARD_REDIS_URL="redis://guard:guard@localhost:6379/0"
|
||||||
JWT_PUBLIC_KEY="ecdsa"
|
|
||||||
|
GUARD_ADMIN_NAME="admin"
|
||||||
|
GUARD_ADMIN_EMAIL="admin@test.net"
|
||||||
|
GUARD_ADMIN_PASSWORD="secret"
|
||||||
|
|
||||||
|
GUARD_JWT_PRIVATE="rsa"
|
||||||
|
GUARD_JWT_PUBLIC="rsa"
|
||||||
|
GUARD_JWT_KID="my-rsa-key-1"
|
||||||
|
|
||||||
|
GUARD_MINIO_ENDPOINT="localhost:9000"
|
||||||
|
GUARD_MINIO_ACCESS_KEY=""
|
||||||
|
GUARD_MINIO_SECRET_KEY=""
|
||||||
|
|
||||||
GOOSE_DRIVER="postgres"
|
GOOSE_DRIVER="postgres"
|
||||||
GOOSE_DBSTRING=$DATABASE_URL
|
GOOSE_DBSTRING=$DATABASE_URL
|
||||||
GOOSE_MIGRATION_DIR="./migrations"
|
GOOSE_MIGRATION_DIR="./migrations"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
bin/*
|
bin/*
|
||||||
|
|
||||||
|
tmp/
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
@@ -25,6 +27,11 @@ go.work.sum
|
|||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
.env.remote
|
||||||
|
|
||||||
# key files
|
# key files
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
|
NUL
|
||||||
|
|
||||||
|
dist/
|
||||||
|
|||||||
55
Dockerfile
Normal file
55
Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Stage 1: Build frontend
|
||||||
|
FROM node:22 AS frontend-builder
|
||||||
|
WORKDIR /app/web
|
||||||
|
COPY web/ .
|
||||||
|
RUN npm install && npm run build
|
||||||
|
|
||||||
|
# Stage 2: Build backend
|
||||||
|
FROM golang:1.24 AS backend-builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
# Copy built frontend into Go embed path (adjust if needed)
|
||||||
|
# COPY --from=frontend-builder /app/web/dist ./web/dist
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux make build
|
||||||
|
|
||||||
|
# Stage 3: Final image
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install CA certificates for HTTPS
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=backend-builder /app/bin/hspguard .
|
||||||
|
COPY --from=frontend-builder /app/dist ./dist
|
||||||
|
|
||||||
|
COPY redis.conf /config/redis.conf
|
||||||
|
|
||||||
|
# Optional: copy default .env file if used
|
||||||
|
# COPY .env .env
|
||||||
|
|
||||||
|
# Set environment variables (can be overridden at runtime)
|
||||||
|
ENV ENV=production \
|
||||||
|
GUARD_PORT=3001 \
|
||||||
|
GUARD_HOST="127.0.0.1" \
|
||||||
|
GUARD_URI="http://localhost:3001" \
|
||||||
|
GUARD_DB_URL="postgres://user:user@localhost:5432/db?sslmode=disable" \
|
||||||
|
GUARD_ADMIN_NAME="admin" \
|
||||||
|
GUARD_ADMIN_EMAIL="admin@test.net" \
|
||||||
|
GUARD_ADMIN_PASSWORD="secret" \
|
||||||
|
GUARD_JWT_PRIVATE="rsa" \
|
||||||
|
GUARD_JWT_PUBLIC="rsa" \
|
||||||
|
GUARD_JWT_KID="my-rsa-key-1" \
|
||||||
|
GUARD_MINIO_ENDPOINT="localhost:9000" \
|
||||||
|
GUARD_MINIO_ACCESS_KEY="" \
|
||||||
|
GUARD_MINIO_SECRET_KEY="" \
|
||||||
|
GOOSE_DRIVER="postgres" \
|
||||||
|
GOOSE_DBSTRING=$GUARD_DB_URL \
|
||||||
|
GOOSE_MIGRATION_DIR="./migrations"
|
||||||
|
|
||||||
|
EXPOSE 3001
|
||||||
|
|
||||||
|
CMD ["./hspguard"]
|
||||||
19
Makefile
19
Makefile
@@ -1,11 +1,14 @@
|
|||||||
|
|
||||||
# Project metadata
|
# Project metadata
|
||||||
APP_NAME := hspguard
|
APP_NAME := hspguard
|
||||||
CMD_DIR := ./cmd/$(APP_NAME)
|
CMD_DIR := ./cmd/$(APP_NAME)
|
||||||
BIN_DIR := ./bin
|
BIN_DIR := ./bin
|
||||||
BIN_PATH := $(BIN_DIR)/$(APP_NAME)
|
|
||||||
|
# Detect platform and add .exe suffix on Windows
|
||||||
|
OS := $(shell go env GOOS)
|
||||||
|
EXT := $(if $(filter windows,$(OS)),.exe,)
|
||||||
|
BIN_PATH := $(BIN_DIR)/$(APP_NAME)$(EXT)
|
||||||
|
|
||||||
PKG := ./...
|
PKG := ./...
|
||||||
GO_FILES := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
|
|
||||||
|
|
||||||
# Go tools
|
# Go tools
|
||||||
GO := go
|
GO := go
|
||||||
@@ -16,21 +19,20 @@ GOTEST := go test
|
|||||||
# Build flags
|
# Build flags
|
||||||
LD_FLAGS := -s -w
|
LD_FLAGS := -s -w
|
||||||
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
|
BUILD_TIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>NUL || echo unknown)
|
||||||
|
|
||||||
.PHONY: all build clean fmt lint run
|
.PHONY: all build clean fmt lint run test mod
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@mkdir -p $(BIN_DIR)
|
|
||||||
$(GO) build -ldflags "-X main.buildTime=$(BUILD_TIME) -X main.commitHash=$(GIT_COMMIT) $(LD_FLAGS)" -o $(BIN_PATH) $(CMD_DIR)
|
$(GO) build -ldflags "-X main.buildTime=$(BUILD_TIME) -X main.commitHash=$(GIT_COMMIT) $(LD_FLAGS)" -o $(BIN_PATH) $(CMD_DIR)
|
||||||
|
|
||||||
run:
|
run:
|
||||||
$(GO) run $(CMD_DIR)
|
$(GO) run $(CMD_DIR)
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
$(GOFMT) -s -w $(GO_FILES)
|
$(GOFMT) -s -w .
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
$(GOLINT) run
|
$(GOLINT) run
|
||||||
@@ -39,8 +41,7 @@ test:
|
|||||||
$(GOTEST) -v $(PKG)
|
$(GOTEST) -v $(PKG)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -rf $(BIN_DIR)
|
@if [ -d "$(BIN_DIR)" ]; then rm -rf $(BIN_DIR); fi
|
||||||
|
|
||||||
mod:
|
mod:
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|
||||||
|
|||||||
168
README.md
168
README.md
@@ -1,123 +1,139 @@
|
|||||||
# HSP Guard
|
|
||||||
|
|
||||||
**HSP Guard** is an internal security service for your home lab, designed to manage user access to various home services and tools. It dynamically controls permissions and prevents unauthorized or unexpected users from accessing sensitive services.
|
# 🛡️ HSP Guard
|
||||||
|
|
||||||
|
**HSP Guard** is a modern OpenID Connect (OIDC) identity provider and access management system for home labs. It provides secure authentication and granular authorization for all your self-hosted services, combining ease of use with enterprise-level control — without any vendor lock-in.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📌 Overview
|
## ✨ Features
|
||||||
|
|
||||||
HSP Guard authorizes user requests and provides an efficient way to:
|
- **OIDC Provider**: Central login for your home lab apps
|
||||||
|
- **Admin UI**: Manage apps, users, roles, permissions, and sessions
|
||||||
- Manage permissions for individual services/tools
|
- **API Tokens**: Issue access tokens with embedded roles and permissions
|
||||||
- Define roles for easier access control
|
- **Flexible Authorization**: Support for roles, permissions, and groups (future)
|
||||||
- Validate and authorize users via JWT tokens
|
- **App Registration**: Register OAuth/OIDC clients with custom permissions
|
||||||
- Securely integrate with new services during installation
|
- **Automatic Permission Sync**: Optionally fetch app permissions from `/.well-known/guard-configuration`
|
||||||
|
- **User & Admin Sessions**: See and revoke active user/app sessions
|
||||||
|
- **Pluggable**: Easily integrate new apps and services
|
||||||
|
- **Audit Logging**: Track actions for security and troubleshooting (planned)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📚 Concepts
|
## 🚀 Getting Started
|
||||||
|
|
||||||
### 🔐 Permission
|
### 1. **Run HSP Guard**
|
||||||
|
|
||||||
Permissions define access to specific features or tools.
|
You can run HSP Guard via Docker, Docker Compose, or natively (see below).
|
||||||
By default, HSP Guard includes predefined administrative permissions that allow an admin to log in and configure the system.
|
|
||||||
|
|
||||||
Once logged in, the admin can:
|
### 2. **Register Your First App**
|
||||||
|
|
||||||
- Manually create new permissions for specific applications
|
1. **Login as admin**
|
||||||
- Allow new applications to register their own permissions
|
2. Go to **Apps → Register New App**
|
||||||
- Assign permissions to users, granting them access to corresponding tools
|
3. Enter:
|
||||||
|
- **Name** of your app
|
||||||
|
- **Redirect URIs** (for OIDC/OAuth callbacks)
|
||||||
|
- (Optional) **Permissions** (manual or auto-discovered from the app)
|
||||||
|
4. Save to receive a `client_id` and `client_secret`
|
||||||
|
5. Configure your app to use these for OIDC login
|
||||||
|
|
||||||
|
### 3. **Assign Permissions & Roles**
|
||||||
|
|
||||||
|
- Assign **default roles** to new users automatically (configurable)
|
||||||
|
- Create custom **roles** to bundle permissions (e.g., `FAMILY_MEMBER`)
|
||||||
|
- Assign users to roles and/or groups for flexible access control
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🧩 Role
|
## 🏗️ Concepts
|
||||||
|
|
||||||
A **Role** is a named collection of permissions (e.g., `GUEST`, `FRIEND`, `FAMILY_MEMBER`) created by the admin.
|
### 🔑 **Permissions**
|
||||||
Roles simplify user management by allowing bulk assignment of permissions. Instead of assigning multiple permissions individually, a role bundles them under one label.
|
Fine-grained controls for app features (e.g., `music.play`, `dashboard.edit`).
|
||||||
|
Can be manually defined or auto-discovered from an app’s `.well-known/guard-configuration` endpoint.
|
||||||
|
|
||||||
|
### 🧩 **Roles**
|
||||||
|
Named bundles of permissions (e.g., `GUEST`, `FAMILY_MEMBER`, `ADMIN`).
|
||||||
|
Assign to users/groups for easier management.
|
||||||
|
|
||||||
|
### 👥 **Groups**
|
||||||
|
(Planned) Logical user collections (e.g., “Family”, “Guests”, “Admins”) for batch management of roles/permissions.
|
||||||
|
|
||||||
|
### 👤 **Users**
|
||||||
|
Each user has a unique profile, roles, and group memberships.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 👥 Group *(Coming Soon)*
|
## 🔗 OIDC/OAuth Integration
|
||||||
|
|
||||||
This feature is planned for future releases. Groups will help organize users or services into logical clusters for simplified access control.
|
**HSP Guard** is a standard-compliant OIDC Provider. Any app supporting OIDC/OAuth can integrate.
|
||||||
|
|
||||||
|
- Register app in admin panel to get `client_id` & `client_secret`
|
||||||
|
- Configure your app’s OIDC integration (see your app’s docs)
|
||||||
|
- Token claims include `permissions` and `roles` for easy authorization
|
||||||
|
|
||||||
|
#### **Example Token Claims**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sub": "123456",
|
||||||
|
"name": "Alex Example",
|
||||||
|
"email": "alex@example.com",
|
||||||
|
"roles": ["GUEST"],
|
||||||
|
"permissions": ["dashboard.view", "music.play"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📡 API
|
## 📡 **App Permission Discovery**
|
||||||
|
|
||||||
### ✅ User Authorization
|
If your app supports permission discovery:
|
||||||
|
- Expose `/.well-known/guard-configuration` endpoint listing available permissions
|
||||||
|
- When registering in HSP Guard, auto-fetch and display for approval
|
||||||
|
|
||||||
To verify whether a request is made by a valid and authorized user, applications can require a **JWT token** as part of the request.
|
#### **Example guard-configuration JSON**
|
||||||
This token is sent to HSP Guard, which:
|
```json
|
||||||
|
{
|
||||||
- Validates the token
|
"permissions": [
|
||||||
- Returns user details (e.g., ID, name, email) for logging, auditing, or request tracing
|
"dashboard.view",
|
||||||
|
"dashboard.edit",
|
||||||
|
"dashboard.admin"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🔑 Permission Checking
|
## 🔄 **User & Admin Sessions**
|
||||||
|
|
||||||
Applications can also verify whether a user holds specific permissions before granting access to certain services or features.
|
- List all active sessions (browser, app, device, timestamp)
|
||||||
|
- Revoke sessions (logout) from user or admin panel
|
||||||
To do this, an app sends:
|
|
||||||
|
|
||||||
- The user's JWT token
|
|
||||||
- A list of required permissions
|
|
||||||
|
|
||||||
HSP Guard checks the user’s assigned permissions and responds with the authorization status.
|
|
||||||
|
|
||||||
> **Best Practice:** Applications should directly integrate with HSP Guard to enforce permission-based access control.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔄 User Authorization Flow
|
## 📦 **Planned Features & Roadmap**
|
||||||
|
|
||||||
When a user tries to access a home lab service that requires authentication:
|
- [ ] **Group Management** for batch assignments
|
||||||
|
- [ ] **Audit Logging** of all admin/user actions
|
||||||
1. The application will **offer an authorization URL** to the user.
|
- [ ] **Permission Expiry** (time-limited access)
|
||||||
2. The user follows the URL and is taken to the **HSP Guard login page**.
|
- [ ] **Advanced Web UI** (dark mode, mobile)
|
||||||
3. The user selects or signs into an account they wish to use for that service.
|
- [ ] **External Identity Providers** (login with Google, GitHub, etc.)
|
||||||
4. Once authenticated and authorized, the user is redirected to the **application-defined redirect URL**.
|
|
||||||
5. The application can now:
|
|
||||||
- Retrieve a **JWT token** from the redirect callback
|
|
||||||
- **Optionally cache the session/token** to avoid prompting the user every time
|
|
||||||
|
|
||||||
This process is similar to how external identity providers like **Google Sign-In** or **GitHub OAuth** work — providing a seamless and secure authentication experience for the user.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚙️ Integrating New Services & Tools
|
## 🛠️ **Development**
|
||||||
|
|
||||||
When a new service or tool is installed:
|
- See [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute!
|
||||||
|
- Pull requests and issues are welcome.
|
||||||
1. It provides a configuration file to HSP Guard
|
|
||||||
2. Guard extracts and registers any defined permissions
|
|
||||||
3. These permissions are **isolated** — even if a name overlaps with existing permissions, a prefix is added to avoid conflicts
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 👤 User Registration & Onboarding
|
## 📝 **License**
|
||||||
|
|
||||||
New users (e.g., family, friends, guests) must complete a registration process to access your home lab.
|
MIT — open source, for the home lab community.
|
||||||
|
|
||||||
They can:
|
|
||||||
|
|
||||||
- Visit a user-friendly registration webpage
|
|
||||||
- Fill out a form with basic information (name, email, password, etc.)
|
|
||||||
|
|
||||||
Once registered, the admin can assign roles or individual permissions as needed.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚧 Roadmap
|
## 💬 **Feedback**
|
||||||
|
|
||||||
- [ ] Group Management
|
Open an [issue](https://github.com/yourusername/hsp-guard/issues) or join the discussion!
|
||||||
- [ ] Web UI Enhancements
|
|
||||||
- [ ] Audit Logging
|
|
||||||
- [ ] Permission Expiry & Time-Based Access
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📬 Feedback & Contribution
|
|
||||||
|
|
||||||
Feel free to open an issue or pull request if you’d like to contribute or report bugs. HSP Guard is a personal home lab project, but feedback is always welcome!
|
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
|
"gitea.local/admin/hspguard/internal/admin"
|
||||||
|
"gitea.local/admin/hspguard/internal/auth"
|
||||||
|
"gitea.local/admin/hspguard/internal/cache"
|
||||||
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
|
"gitea.local/admin/hspguard/internal/oauth"
|
||||||
"gitea.local/admin/hspguard/internal/repository"
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
|
"gitea.local/admin/hspguard/internal/storage"
|
||||||
"gitea.local/admin/hspguard/internal/user"
|
"gitea.local/admin/hspguard/internal/user"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
@@ -16,12 +21,18 @@ import (
|
|||||||
type APIServer struct {
|
type APIServer struct {
|
||||||
addr string
|
addr string
|
||||||
repo *repository.Queries
|
repo *repository.Queries
|
||||||
|
storage *storage.FileStorage
|
||||||
|
cache *cache.Client
|
||||||
|
cfg *config.AppConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIServer(addr string, db *repository.Queries) *APIServer {
|
func NewAPIServer(addr string, db *repository.Queries, minio *storage.FileStorage, cache *cache.Client, cfg *config.AppConfig) *APIServer {
|
||||||
return &APIServer{
|
return &APIServer{
|
||||||
addr: addr,
|
addr: addr,
|
||||||
repo: db,
|
repo: db,
|
||||||
|
storage: minio,
|
||||||
|
cache: cache,
|
||||||
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,13 +40,35 @@ func (s *APIServer) Run() error {
|
|||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
router.Use(middleware.Logger)
|
router.Use(middleware.Logger)
|
||||||
|
|
||||||
workDir, _ := os.Getwd()
|
// workDir, _ := os.Getwd()
|
||||||
staticDir := http.Dir(filepath.Join(workDir, "static"))
|
// staticDir := http.Dir(filepath.Join(workDir, "static"))
|
||||||
FileServer(router, "/static", staticDir)
|
// FileServer(router, "/static", staticDir)
|
||||||
|
|
||||||
|
oauthHandler := oauth.NewOAuthHandler(s.repo, s.cache, s.cfg)
|
||||||
|
|
||||||
router.Route("/api/v1", func(r chi.Router) {
|
router.Route("/api/v1", func(r chi.Router) {
|
||||||
userHandler := user.NewUserHandler(s.repo)
|
userHandler := user.NewUserHandler(s.repo, s.storage, s.cfg)
|
||||||
userHandler.RegisterRoutes(router, r)
|
userHandler.RegisterRoutes(r)
|
||||||
|
|
||||||
|
authHandler := auth.NewAuthHandler(s.repo, s.cache, s.cfg)
|
||||||
|
authHandler.RegisterRoutes(r)
|
||||||
|
|
||||||
|
oauthHandler.RegisterRoutes(r)
|
||||||
|
|
||||||
|
adminHandler := admin.New(s.repo, s.cfg)
|
||||||
|
adminHandler.RegisterRoutes(r)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get("/.well-known/jwks.json", oauthHandler.WriteJWKS)
|
||||||
|
router.Get("/.well-known/openid-configuration", oauthHandler.OpenIdConfiguration)
|
||||||
|
|
||||||
|
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := "./dist" + r.URL.Path
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
http.ServeFile(w, r, "./dist/index.html")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.FileServer(http.Dir("./dist")).ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle unknown routes
|
// Handle unknown routes
|
||||||
@@ -48,7 +81,7 @@ func (s *APIServer) Run() error {
|
|||||||
router.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
|
router.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
fmt.Fprint(w, `{"error": "405 - method not allowed"}`)
|
_, _ = fmt.Fprint(w, `{"error": "405 - method not allowed"}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Println("Listening on", s.addr)
|
log.Println("Listening on", s.addr)
|
||||||
|
|||||||
@@ -7,22 +7,32 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"gitea.local/admin/hspguard/cmd/hspguard/api"
|
"gitea.local/admin/hspguard/cmd/hspguard/api"
|
||||||
|
"gitea.local/admin/hspguard/internal/cache"
|
||||||
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
"gitea.local/admin/hspguard/internal/repository"
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
|
"gitea.local/admin/hspguard/internal/storage"
|
||||||
"gitea.local/admin/hspguard/internal/user"
|
"gitea.local/admin/hspguard/internal/user"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
|
if err != nil && os.Getenv("ENV") != "production" {
|
||||||
|
log.Fatalln("WARNING: .env file not found. Skipping...")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg config.AppConfig
|
||||||
|
|
||||||
|
err = config.LoadEnv(&cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("ERR: Failed to load environment variables:", err)
|
log.Fatal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
conn, err := pgx.Connect(ctx, os.Getenv("DATABASE_URL"))
|
conn, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("ERR: Failed to connect to db:", err)
|
log.Fatalln("ERR: Failed to connect to db:", err)
|
||||||
return
|
return
|
||||||
@@ -30,14 +40,14 @@ func main() {
|
|||||||
|
|
||||||
repo := repository.New(conn)
|
repo := repository.New(conn)
|
||||||
|
|
||||||
user.EnsureAdminUser(ctx, repo)
|
fStorage := storage.New(&cfg)
|
||||||
|
|
||||||
port := os.Getenv("PORT")
|
cache := cache.NewClient(&cfg)
|
||||||
if port == "" {
|
|
||||||
port = "3000"
|
|
||||||
}
|
|
||||||
|
|
||||||
server := api.NewAPIServer(fmt.Sprintf(":%s", port), repo)
|
user.EnsureAdminUser(ctx, &cfg, repo)
|
||||||
|
user.EnsureSystemPermissions(ctx, repo)
|
||||||
|
|
||||||
|
server := api.NewAPIServer(fmt.Sprintf("%s:%s", cfg.Host, cfg.Port), repo, fStorage, cache, &cfg)
|
||||||
if err := server.Run(); err != nil {
|
if err := server.Run(); err != nil {
|
||||||
log.Fatalln("ERR: Failed to start server:", err)
|
log.Fatalln("ERR: Failed to start server:", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres
|
image: postgres
|
||||||
@@ -7,6 +6,24 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: guard
|
POSTGRES_USER: guard
|
||||||
POSTGRES_PASSWORD: guard
|
POSTGRES_PASSWORD: guard
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
|
||||||
|
cache:
|
||||||
|
image: redis:7.2 # or newer
|
||||||
|
container_name: guard-redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
- ./redis.conf:/usr/local/etc/redis/redis.conf
|
||||||
|
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis-data:
|
||||||
|
driver: local
|
||||||
|
postgres-data:
|
||||||
|
driver: local
|
||||||
|
|||||||
34
go.mod
34
go.mod
@@ -3,15 +3,35 @@ module gitea.local/admin/hspguard
|
|||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgx/v5 v5.7.5
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/avct/uasurfer v0.0.0-20250506104815-f2613aa2d406 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx v3.6.2+incompatible // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/minio/crc64nvme v1.0.1 // indirect
|
||||||
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/minio/minio-go/v7 v7.0.92 // indirect
|
||||||
|
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0 // indirect
|
||||||
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
|
github.com/tinylib/msgp v1.3.0 // indirect
|
||||||
golang.org/x/crypto v0.38.0 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
golang.org/x/net v0.38.0 // indirect
|
||||||
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
51
go.sum
51
go.sum
@@ -1,6 +1,20 @@
|
|||||||
|
github.com/avct/uasurfer v0.0.0-20250506104815-f2613aa2d406 h1:5/KfwL9TS8yNtUSunutqifcSC8rdX9PNdvbSsw/X/lQ=
|
||||||
|
github.com/avct/uasurfer v0.0.0-20250506104815-f2613aa2d406/go.mod h1:s+GCtuP4kZNxh1WGoqdWI1+PbluBcycrMMWuKQ9e5Nk=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -9,21 +23,50 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||||||
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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
|
|
||||||
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
|
||||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
|
||||||
|
github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.92 h1:jpBFWyRS3p8P/9tsRc+NuvqoFi7qAmTCFPoRFmobbVw=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.92/go.mod h1:vTIc8DNcnAZIhyFsk8EB90AbPjj3j68aWIEQCiPj7d0=
|
||||||
|
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
|
||||||
|
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
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/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.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.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||||
|
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
312
internal/admin/apiservices.go
Normal file
312
internal/admin/apiservices.go
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
|
"gitea.local/admin/hspguard/internal/types"
|
||||||
|
"gitea.local/admin/hspguard/internal/util"
|
||||||
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetApiServices(w http.ResponseWriter, r *http.Request) {
|
||||||
|
services, err := h.repo.ListApiServices(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to list api services from db:", err)
|
||||||
|
web.Error(w, "failed to get api services", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServices := make([]types.ApiServiceDTO, 0)
|
||||||
|
|
||||||
|
for _, service := range services {
|
||||||
|
apiServices = append(apiServices, types.NewApiServiceDTO(service))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Items []types.ApiServiceDTO `json:"items"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(Response{
|
||||||
|
Items: apiServices,
|
||||||
|
Count: len(apiServices),
|
||||||
|
}); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddServiceRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
RedirectUris []string `json:"redirect_uris"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
GrantTypes []string `json:"grant_types"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiServiceCredentials struct {
|
||||||
|
ClientId string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) AddApiService(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req AddServiceRequest
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
|
||||||
|
if err := decoder.Decode(&req); err != nil {
|
||||||
|
web.Error(w, "failed to parse request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
web.Error(w, "name is required for an api service", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId, err := util.GenerateClientID()
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "failed to generate client id", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSecret, err := util.GenerateClientSecret()
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "failed to generate client secret", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashSecret, err := util.HashPassword(clientSecret)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "failed to create client secret", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := repository.CreateApiServiceParams{
|
||||||
|
ClientID: clientId,
|
||||||
|
ClientSecret: hashSecret,
|
||||||
|
Name: req.Name,
|
||||||
|
RedirectUris: req.RedirectUris,
|
||||||
|
Scopes: req.Scopes,
|
||||||
|
GrantTypes: req.GrantTypes,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Description != "" {
|
||||||
|
params.Description = &req.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := h.repo.CreateApiService(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "failed to create new api service", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
service.ClientSecret = clientSecret
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Service types.ApiServiceDTO `json:"service"`
|
||||||
|
Credentials ApiServiceCredentials `json:"credentials"`
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(Response{
|
||||||
|
Service: types.NewApiServiceDTO(service),
|
||||||
|
Credentials: ApiServiceCredentials{
|
||||||
|
ClientId: service.ClientID,
|
||||||
|
ClientSecret: service.ClientSecret,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetApiService(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serviceId := chi.URLParam(r, "id")
|
||||||
|
parsed, err := uuid.Parse(serviceId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "service id provided is not a valid uuid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := h.repo.GetApiServiceId(r.Context(), parsed)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "service with provided id not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(types.NewApiServiceDTO(service)); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetApiServiceCID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientId := chi.URLParam(r, "client_id")
|
||||||
|
|
||||||
|
service, err := h.repo.GetApiServiceCID(r.Context(), clientId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "service with provided client id not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(types.NewApiServiceDTO(service)); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) RegenerateApiServiceSecret(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serviceId := chi.URLParam(r, "id")
|
||||||
|
parsed, err := uuid.Parse(serviceId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "provided service id is not valid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := h.repo.GetApiServiceId(r.Context(), parsed)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "service with provided id not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSecret, err := util.GenerateClientSecret()
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "failed to generate client secret", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.UpdateClientSecret(r.Context(), repository.UpdateClientSecretParams{
|
||||||
|
ClientID: service.ClientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
}); err != nil {
|
||||||
|
web.Error(w, "failed to update client secret for service", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(ApiServiceCredentials{
|
||||||
|
ClientId: service.ClientID,
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
}); err != nil {
|
||||||
|
web.Error(w, "failed to send credentials", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateApiServiceRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
RedirectUris []string `json:"redirect_uris"`
|
||||||
|
Scopes []string `json:"scopes"`
|
||||||
|
GrantTypes []string `json:"grant_types"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) UpdateApiService(w http.ResponseWriter, r *http.Request) {
|
||||||
|
serviceId := chi.URLParam(r, "id")
|
||||||
|
parsed, err := uuid.Parse(serviceId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "provided service id is not valid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateApiServiceRequest
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(&req); err != nil {
|
||||||
|
web.Error(w, "missing required fields to update service", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name == "" {
|
||||||
|
web.Error(w, "service name is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Scopes) == 0 {
|
||||||
|
web.Error(w, "at least 1 scope is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := h.repo.GetApiServiceId(r.Context(), parsed)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "service with provided id not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := h.repo.UpdateApiService(r.Context(), repository.UpdateApiServiceParams{
|
||||||
|
ClientID: service.ClientID,
|
||||||
|
Name: req.Name,
|
||||||
|
Description: &req.Description,
|
||||||
|
RedirectUris: req.RedirectUris,
|
||||||
|
Scopes: req.Scopes,
|
||||||
|
GrantTypes: req.GrantTypes,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "failed to update api service", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(types.NewApiServiceDTO(updated)); err != nil {
|
||||||
|
web.Error(w, "failed to send updated api service", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) ToggleApiService(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
serviceId := chi.URLParam(r, "id")
|
||||||
|
parsed, err := uuid.Parse(serviceId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "provided service id is not valid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := h.repo.GetApiServiceId(r.Context(), parsed)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "service with provided id not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.IsActive {
|
||||||
|
log.Println("INFO: Service is active. Deactivating...")
|
||||||
|
err = h.repo.DeactivateApiService(r.Context(), service.ClientID)
|
||||||
|
} else {
|
||||||
|
log.Println("INFO: Service is inactive. Activating...")
|
||||||
|
err = h.repo.ActivateApiService(r.Context(), service.ClientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERR: Failed to toggle api service (cid: %s): %v\n", service.ClientID, err)
|
||||||
|
web.Error(w, "failed to toggle api service", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
82
internal/admin/permissions.go
Normal file
82
internal/admin/permissions.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetAllPermissions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rows, err := h.repo.GetGroupedPermissions(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to list permissions from db:", err)
|
||||||
|
web.Error(w, "failed to get all permissions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowDTO struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Permissions []repository.Permission `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
rows = make([]repository.GetGroupedPermissionsRow, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapped []RowDTO
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
var permissions []repository.Permission
|
||||||
|
|
||||||
|
if err := json.Unmarshal(row.Permissions, &permissions); err != nil {
|
||||||
|
log.Println("ERR: Failed to extract permissions from byte array:", err)
|
||||||
|
web.Error(w, "failed to get permissions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped = append(mapped, RowDTO{
|
||||||
|
Scope: row.Scope,
|
||||||
|
Permissions: permissions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mapped) == 0 {
|
||||||
|
mapped = make([]RowDTO, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(mapped); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetUserPermissions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userId := chi.URLParam(r, "user_id")
|
||||||
|
|
||||||
|
permissions, err := h.repo.GetUserPermissions(r.Context(), uuid.MustParse(userId))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to list permissions from db:", err)
|
||||||
|
web.Error(w, "failed to get user permissions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(permissions) == 0 {
|
||||||
|
permissions = make([]repository.Permission, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(permissions); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
156
internal/admin/roles.go
Normal file
156
internal/admin/roles.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetAllRoles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rows, err := h.repo.GetRolesGroupedWithPermissions(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to list roles from db:", err)
|
||||||
|
web.Error(w, "failed to get all roles", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type RolePermissions struct {
|
||||||
|
Permissions []repository.Permission `json:"permissions"`
|
||||||
|
repository.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
type RowDTO struct {
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
Roles []RolePermissions `json:"roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
rows = make([]repository.GetRolesGroupedWithPermissionsRow, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapped []RowDTO
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
var mappedRow RowDTO
|
||||||
|
|
||||||
|
mappedRow.Scope = row.Scope
|
||||||
|
|
||||||
|
if err := json.Unmarshal(row.Roles, &mappedRow.Roles); err != nil {
|
||||||
|
log.Println("ERR: Failed to extract roles from byte array:", err)
|
||||||
|
web.Error(w, "failed to get roles", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped = append(mapped, mappedRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mapped) == 0 {
|
||||||
|
mapped = make([]RowDTO, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(mapped); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetUserRoles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userId := chi.URLParam(r, "user_id")
|
||||||
|
|
||||||
|
parsed, err := uuid.Parse(userId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERR: Received invalid UUID on get user roles '%s': %v\n", userId, err)
|
||||||
|
web.Error(w, "invalid user id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := h.repo.GetUserRoles(r.Context(), parsed)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to list roles from db:", err)
|
||||||
|
web.Error(w, "failed to get user roles", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
rows = make([]repository.Role, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(rows); err != nil {
|
||||||
|
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AssignRoleRequest struct {
|
||||||
|
RoleKey string `json:"role_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) AssignUserRole(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userId := chi.URLParam(r, "user_id")
|
||||||
|
|
||||||
|
var req AssignRoleRequest
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
|
||||||
|
if err := decoder.Decode(&req); err != nil {
|
||||||
|
web.Error(w, "failed to parse request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RoleKey == "" {
|
||||||
|
web.Error(w, "role key is required for assign", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := uuid.Parse(userId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERR: Failed to parse provided user ID '%s': %v\n", userId, err)
|
||||||
|
web.Error(w, "invalid user id provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.repo.FindUserId(r.Context(), parsed)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "no user found under provided id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.repo.FindUserRole(r.Context(), repository.FindUserRoleParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Key: req.RoleKey,
|
||||||
|
}); err == nil {
|
||||||
|
log.Printf("INFO: Unassigning role '%s' for user with '%s' id", req.RoleKey, user.ID.String())
|
||||||
|
// Unassign Role
|
||||||
|
if err := h.repo.UnassignUserRole(r.Context(), repository.UnassignUserRoleParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Key: req.RoleKey,
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("ERR: Failed to unassign role '%s' from user with '%s' id: %v\n", req.RoleKey, user.ID.String(), err)
|
||||||
|
web.Error(w, "failed to unassign role to user", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("INFO: Assigning role '%s' for user with '%s' id", req.RoleKey, user.ID.String())
|
||||||
|
if err := h.repo.AssignUserRole(r.Context(), repository.AssignUserRoleParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Key: req.RoleKey,
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("ERR: Failed to assign role '%s' to user with '%s' id: %v\n", req.RoleKey, user.ID.String(), err)
|
||||||
|
web.Error(w, "failed to assign role to user", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
54
internal/admin/routes.go
Normal file
54
internal/admin/routes.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
|
imiddleware "gitea.local/admin/hspguard/internal/middleware"
|
||||||
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminHandler struct {
|
||||||
|
repo *repository.Queries
|
||||||
|
cfg *config.AppConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(repo *repository.Queries, cfg *config.AppConfig) *AdminHandler {
|
||||||
|
return &AdminHandler{
|
||||||
|
repo,
|
||||||
|
cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) RegisterRoutes(router chi.Router) {
|
||||||
|
router.Route("/admin", func(r chi.Router) {
|
||||||
|
authMiddleware := imiddleware.NewAuthMiddleware(h.cfg, h.repo)
|
||||||
|
adminMiddleware := imiddleware.NewAdminMiddleware(h.repo)
|
||||||
|
r.Use(authMiddleware.Runner, adminMiddleware.Runner)
|
||||||
|
|
||||||
|
r.Get("/api-services", h.GetApiServices)
|
||||||
|
r.Get("/api-services/{id}", h.GetApiService)
|
||||||
|
r.Post("/api-services", h.AddApiService)
|
||||||
|
r.Patch("/api-services/{id}", h.RegenerateApiServiceSecret)
|
||||||
|
r.Put("/api-services/{id}", h.UpdateApiService)
|
||||||
|
r.Patch("/api-services/toggle/{id}", h.ToggleApiService)
|
||||||
|
|
||||||
|
r.Get("/users", h.GetUsers)
|
||||||
|
r.Post("/users", h.CreateUser)
|
||||||
|
r.Get("/users/{id}", h.GetUser)
|
||||||
|
|
||||||
|
r.Get("/user-sessions", h.GetUserSessions)
|
||||||
|
r.Patch("/user-sessions/revoke/{id}", h.RevokeUserSession)
|
||||||
|
|
||||||
|
r.Get("/service-sessions", h.GetServiceSessions)
|
||||||
|
r.Patch("/service-sessions/revoke/{id}", h.RevokeUserSession)
|
||||||
|
|
||||||
|
r.Get("/permissions", h.GetAllPermissions)
|
||||||
|
r.Get("/permissions/{user_id}", h.GetUserPermissions)
|
||||||
|
|
||||||
|
r.Get("/roles", h.GetAllRoles)
|
||||||
|
r.Get("/roles/{user_id}", h.GetUserRoles)
|
||||||
|
r.Patch("/roles/{user_id}", h.AssignUserRole)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get("/api-services/client/{client_id}", h.GetApiServiceCID)
|
||||||
|
}
|
||||||
182
internal/admin/sessions.go
Normal file
182
internal/admin/sessions.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
|
"gitea.local/admin/hspguard/internal/types"
|
||||||
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetSessionsParams struct {
|
||||||
|
PageSize int `json:"size"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
// TODO: More filtering possibilities like onlyActive, expired, not-expired etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetUserSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
|
||||||
|
params := GetSessionsParams{}
|
||||||
|
|
||||||
|
if pageSize, err := strconv.Atoi(q.Get("size")); err == nil {
|
||||||
|
params.PageSize = pageSize
|
||||||
|
} else {
|
||||||
|
params.PageSize = 15
|
||||||
|
}
|
||||||
|
|
||||||
|
if page, err := strconv.Atoi(q.Get("page")); err == nil {
|
||||||
|
params.Page = page
|
||||||
|
} else {
|
||||||
|
web.Error(w, "page is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions, err := h.repo.GetUserSessions(r.Context(), repository.GetUserSessionsParams{
|
||||||
|
Limit: int32(params.PageSize),
|
||||||
|
Offset: int32(params.Page-1) * int32(params.PageSize),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to read user sessions from db:", err)
|
||||||
|
web.Error(w, "failed to retrieve sessions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSessions, err := h.repo.GetUserSessionsCount(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to get total count of user sessions:", err)
|
||||||
|
web.Error(w, "failed to retrieve sessions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped := make([]*types.UserSessionDTO, 0)
|
||||||
|
|
||||||
|
for _, session := range sessions {
|
||||||
|
mapped = append(mapped, types.NewUserSessionDTO(&session))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Items []*types.UserSessionDTO `json:"items"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
response := Response{
|
||||||
|
Items: mapped,
|
||||||
|
Page: params.Page,
|
||||||
|
TotalPages: int(math.Ceil(float64(totalSessions) / float64(params.PageSize))),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
|
log.Println("ERR: Failed to encode sessions in response:", err)
|
||||||
|
web.Error(w, "failed to encode sessions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) RevokeUserSession(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sessionId := chi.URLParam(r, "id")
|
||||||
|
parsed, err := uuid.Parse(sessionId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "provided service id is not valid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.RevokeUserSession(r.Context(), parsed); err != nil {
|
||||||
|
log.Println("ERR: Failed to revoke user session:", err)
|
||||||
|
web.Error(w, "failed to revoke user session", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("{\"success\":true}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetServiceSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
|
||||||
|
params := GetSessionsParams{}
|
||||||
|
|
||||||
|
if pageSize, err := strconv.Atoi(q.Get("size")); err == nil {
|
||||||
|
params.PageSize = pageSize
|
||||||
|
} else {
|
||||||
|
params.PageSize = 15
|
||||||
|
}
|
||||||
|
|
||||||
|
if page, err := strconv.Atoi(q.Get("page")); err == nil {
|
||||||
|
params.Page = page
|
||||||
|
} else {
|
||||||
|
web.Error(w, "page is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions, err := h.repo.GetServiceSessions(r.Context(), repository.GetServiceSessionsParams{
|
||||||
|
Limit: int32(params.PageSize),
|
||||||
|
Offset: int32(params.Page-1) * int32(params.PageSize),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to read api sessions from db:", err)
|
||||||
|
web.Error(w, "failed to retrieve sessions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSessions, err := h.repo.GetServiceSessionsCount(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to get total count of service sessions:", err)
|
||||||
|
web.Error(w, "failed to retrieve sessions", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped := make([]*types.ServiceSessionDTO, 0)
|
||||||
|
|
||||||
|
for _, session := range sessions {
|
||||||
|
mapped = append(mapped, types.NewServiceSessionDTO(&session))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Items []*types.ServiceSessionDTO `json:"items"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
response := Response{
|
||||||
|
Items: mapped,
|
||||||
|
Page: params.Page,
|
||||||
|
TotalPages: int(math.Ceil(float64(totalSessions) / float64(params.PageSize))),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
|
log.Println("ERR: Failed to encode sessions in response:", err)
|
||||||
|
web.Error(w, "failed to encode sessions", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) RevokeServiceSession(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sessionId := chi.URLParam(r, "id")
|
||||||
|
parsed, err := uuid.Parse(sessionId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "provided service id is not valid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.RevokeServiceSession(r.Context(), parsed); err != nil {
|
||||||
|
log.Println("ERR: Failed to revoke service session:", err)
|
||||||
|
web.Error(w, "failed to revoke service session", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("{\"success\":true}"))
|
||||||
|
}
|
||||||
165
internal/admin/users.go
Normal file
165
internal/admin/users.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
|
"gitea.local/admin/hspguard/internal/types"
|
||||||
|
"gitea.local/admin/hspguard/internal/util"
|
||||||
|
"gitea.local/admin/hspguard/internal/web"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userId, ok := util.GetRequestUserId(r.Context())
|
||||||
|
if !ok {
|
||||||
|
web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId))
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "failed to get access information", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := h.repo.FindAdminUsers(r.Context(), &user.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("ERR: Failed to query users from db:", err)
|
||||||
|
web.Error(w, "failed to get all users", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Items []types.UserDTO `json:"items"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []types.UserDTO
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
items = append(items, types.NewUserDTO(&user))
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(&Response{
|
||||||
|
Items: items,
|
||||||
|
Count: len(items),
|
||||||
|
}); err != nil {
|
||||||
|
web.Error(w, "failed to send response", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminHandler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userId := chi.URLParam(r, "id")
|
||||||
|
parsed, err := uuid.Parse(userId)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "user id provided is not a valid uuid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.repo.FindUserId(r.Context(), parsed)
|
||||||
|
if err != nil {
|
||||||
|
web.Error(w, "user with provided id not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := encoder.Encode(types.NewUserDTO(&user)); err != nil {
|
||||||
|
web.Error(w, "failed to encode user dto", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||