forked from Nixius/authelia
Add ss-atlas service, config updates, ignore IDE cruft
Made-with: Cursor
This commit is contained in:
parent
9bbec9a8d2
commit
ac24f6d1dc
|
|
@ -1,4 +1,5 @@
|
|||
# Secrets and sensitive files
|
||||
.env
|
||||
secrets.md
|
||||
*.secret
|
||||
*.key
|
||||
|
|
@ -26,6 +27,7 @@ logs/
|
|||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.temp
|
||||
.!*
|
||||
# OAuth and other secrets - never commit!
|
||||
secrets/
|
||||
|
|
|
|||
|
|
@ -10,6 +10,15 @@ clone:
|
|||
recursive: true
|
||||
|
||||
steps:
|
||||
# ss-atlas unit tests (runs on every push)
|
||||
ss-atlas-test:
|
||||
name: ss-atlas-test
|
||||
image: golang:1.23-alpine
|
||||
commands:
|
||||
- cd docker/ss-atlas && go test ./...
|
||||
when:
|
||||
event: push
|
||||
|
||||
# Build and Push for Staging
|
||||
build-push-staging:
|
||||
name: build-push-staging
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ Use OAuth for better user experience and native service integration:
|
|||
# Portainer with OAuth - no Traefik middleware needed
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.portainer.rule: "Host(`portainer.nixc.us`)"
|
||||
traefik.http.routers.portainer.rule: "Host(`portainer.a250.ca`)"
|
||||
# OAuth configured in Portainer admin panel
|
||||
```
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ Use Authelia middleware for services without OAuth support:
|
|||
```yaml
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.myapp.rule: "Host(`myapp.nixc.us`)"
|
||||
traefik.http.routers.myapp.rule: "Host(`myapp.a250.ca`)"
|
||||
traefik.http.routers.myapp.middlewares: "authelia_authelia@docker"
|
||||
traefik.http.services.myapp.loadbalancer.server.port: "8080"
|
||||
```
|
||||
|
|
@ -182,7 +182,7 @@ labels:
|
|||
```yaml
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.http.routers.headscale.rule: "Host(`headscale.nixc.us`)"
|
||||
traefik.http.routers.headscale.rule: "Host(`headscale.a250.ca`)"
|
||||
traefik.http.routers.headscale.entrypoints: "websecure"
|
||||
traefik.http.routers.headscale.tls.certresolver: "letsencryptresolver"
|
||||
traefik.http.services.headscale.loadbalancer.server.port: "8080"
|
||||
|
|
|
|||
|
|
@ -17,16 +17,16 @@ authentication_backend:
|
|||
path: /config/users_database.yml
|
||||
|
||||
access_control:
|
||||
default_policy: one_factor
|
||||
default_policy: bypass
|
||||
rules:
|
||||
- domain: ["dev.local.com"]
|
||||
policy: one_factor
|
||||
- domain: ["bc.a250.ca"]
|
||||
policy: bypass
|
||||
|
||||
session:
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: dev.local.com
|
||||
authelia_url: http://dev.local.com:9091
|
||||
domain: bc.a250.ca
|
||||
authelia_url: http://bc.a250.ca:9091
|
||||
secret: DoXL9Z1aCrXQ3Ylc2J9MWLO8QeseI8W6F91R0lS0SIE=
|
||||
expiration: 1h
|
||||
inactivity: 5m
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ services:
|
|||
networks:
|
||||
- authelia_dev
|
||||
healthcheck:
|
||||
test: ["CMD", "/usr/local/bin/healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized"]
|
||||
test: [ "CMD", "/usr/local/bin/healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized" ]
|
||||
start_period: 30s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
|
|
@ -29,7 +29,7 @@ services:
|
|||
networks:
|
||||
- authelia_dev
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
start_period: 10s
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
|
|
@ -43,16 +43,21 @@ services:
|
|||
environment:
|
||||
- LLDAP_JWT_SECRET=I2sNvGvhzZlTJWPfNL9MBPFGhyG/gWU5wHz6wFsIC3I=
|
||||
- LLDAP_LDAP_USER_PASS=/ETAToLiZPWo6QK171abAUqsa3WDpd9IgneZnTA4zU0=
|
||||
- LLDAP_LDAP_BASE_DN=dc=nixc,dc=us
|
||||
- LLDAP_LDAP_BASE_DN=dc=a250,dc=ca
|
||||
- PUID=33
|
||||
- PGID=33
|
||||
ports:
|
||||
# Only expose web UI for manual testing
|
||||
- "17170:17170" # Web interface port
|
||||
- "17170:17170" # Web interface port
|
||||
networks:
|
||||
- authelia_dev
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.lldap.rule=Host(`lldap.bc.a250.ca`)"
|
||||
- "traefik.http.routers.lldap.entrypoints=web"
|
||||
- "traefik.http.services.lldap.loadbalancer.server.port=17170"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:17170/health"]
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:17170/health" ]
|
||||
start_period: 10s
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
|
|
@ -62,8 +67,9 @@ services:
|
|||
build:
|
||||
context: ./docker/authelia/
|
||||
dockerfile: Dockerfile
|
||||
image: git.nixc.us/nixius/authelia:dev-authelia
|
||||
image: git.nixc.us/a250/authelia:dev-authelia
|
||||
container_name: authelia_dev_main
|
||||
user: root
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
|
|
@ -80,15 +86,28 @@ services:
|
|||
echo "$${IDENTITY_PROVIDERS_OIDC_JWKS_KEY}" > /run/secrets/IDENTITY_PROVIDERS_OIDC_JWKS_KEY
|
||||
echo "$${CLIENT_SECRET_HEADSCALE}" > /run/secrets/CLIENT_SECRET_HEADSCALE
|
||||
echo "$${CLIENT_SECRET_HEADADMIN}" > /run/secrets/CLIENT_SECRET_HEADADMIN
|
||||
# Start Authelia with original command
|
||||
exec authelia --config=/config/configuration.server.yml --config=/config/configuration.ldap.yml --config=/config/configuration.acl.yml
|
||||
echo "$${CLIENT_SECRET_PORTAINER}" > /run/secrets/CLIENT_SECRET_PORTAINER
|
||||
echo "$${CLIENT_SECRET_GITEA}" > /run/secrets/CLIENT_SECRET_GITEA
|
||||
|
||||
# Override configuration for local dev
|
||||
printf "notifier:\n filesystem:\n filename: /data/notification.txt\n" > /config/configuration.notifier.yml
|
||||
printf "access_control:\n default_policy: bypass\n rules:\n - domain: [\"*.bc.a250.ca\", \"bc.a250.ca\"]\n policy: bypass\n" > /config/configuration.acl.yml
|
||||
|
||||
# Start Authelia with dev overrides
|
||||
exec authelia \
|
||||
--config=/config/configuration.server.yml \
|
||||
--config=/config/configuration.ldap.yml \
|
||||
--config=/config/configuration.acl.yml \
|
||||
--config=/config/configuration.notifier.yml \
|
||||
--config=/config/configuration.identity.providers.yml \
|
||||
--config=/config/configuration.oidc.clients.yml
|
||||
environment:
|
||||
# Template environment variables
|
||||
X_AUTHELIA_EMAIL: authelia@nixc.us
|
||||
X_AUTHELIA_SITE_NAME: ATLAS-DEV
|
||||
X_AUTHELIA_EMAIL: authelia@a250.ca
|
||||
X_AUTHELIA_SITE_NAME: a250.ca
|
||||
X_AUTHELIA_CONFIG_FILTERS: template
|
||||
X_AUTHELIA_LDAP_DOMAIN: dc=nixc,dc=us
|
||||
TRAEFIK_DOMAIN: dev.local.com
|
||||
X_AUTHELIA_LDAP_DOMAIN: dc=a250,dc=ca
|
||||
TRAEFIK_DOMAIN: bc.a250.ca
|
||||
# Development secrets for templates
|
||||
IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: DoXL9Z1aCrXQ3Ylc2J9MWLO8QeseI8W6F91R0lS0SIE=
|
||||
STORAGE_ENCRYPTION_KEY: DvbtMjsNDIC3eqtNaPtdHm/f07dtlHREgieDStTu9NA=
|
||||
|
|
@ -121,19 +140,29 @@ services:
|
|||
c/W/dqF5xfmVQR0Af/ijs6+Jfjr0NBrT+sHHk+ef8Ktaw8IHslNa6r5TJg82mO2e
|
||||
g7pksppAWxMfKCqUhrDXGgwyFIXpfBT2jkzV530l4+2L5HJK2RO74mNWWHtGcSQF
|
||||
d3VW3WQfqeaj0YK+Oqqf/nHIokG0a2E/4BBjshECgYAnlU2Fl7uI1lQBbWsckaQ9
|
||||
EVeSDtrRvNuER0Eh3WFni9affOqB9qAZXNfCZ+goFJoNgk4fww0OqmewX9Y18/3a
|
||||
EVeSDtrRvNuER0Eh3WFni2affOqB9qAZXNfCZ/goFJoNgk4fww0OqmewX9Y18/3a
|
||||
vsrm7L7OKFFlM6vmIG1nPX/s5l++mkMe+qRd4B7C4NSF0bzJlweTozQFDp+prp1y
|
||||
SHERk3EUdAZn7yyIISd/Qg==
|
||||
-----END PRIVATE KEY-----
|
||||
IDENTITY_PROVIDERS_OIDC_JWKS_KEY: mbfKKlpQ5QEzrmBCCcOg7yubDBKZtKCAiL7rGtVdMq/hpCorO+Qiei2fKbB/xieDS3BIg5BMza5fZm5w0hMiNA==
|
||||
CLIENT_SECRET_HEADSCALE: t4Hvp6DnpA0T+0ePbdx8lPIAujFMrkjEnx5aMQkMFiA=
|
||||
CLIENT_SECRET_HEADADMIN: RAxwkJxwMBSYkaA0r+D5qZdEFIrVEZJbigOPtkCBED8=
|
||||
CLIENT_SECRET_PORTAINER: t4Hvp6DnpA0T+0ePbdx8lPIAujFMrkjEnx5aMQkMFiA=
|
||||
CLIENT_SECRET_GITEA: t4Hvp6DnpA0T+0ePbdx8lPIAujFMrkjEnx5aMQkMFiA=
|
||||
volumes:
|
||||
- authelia_data:/data
|
||||
ports:
|
||||
- "9091:9091"
|
||||
networks:
|
||||
- authelia_dev
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.authelia.rule=Host(`login.bc.a250.ca`)"
|
||||
- "traefik.http.routers.authelia.entrypoints=web"
|
||||
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
|
||||
- "traefik.http.middlewares.authelia-auth.forwardauth.address=http://authelia_dev_main:9091/api/verify?rd=http://login.bc.a250.ca/"
|
||||
- "traefik.http.middlewares.authelia-auth.forwardauth.trustForwardHeader=true"
|
||||
- "traefik.http.middlewares.authelia-auth.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
|
@ -142,12 +171,73 @@ services:
|
|||
lldap:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9091/api/health"]
|
||||
test: [ "CMD-SHELL", "/usr/bin/wget --spider --quiet http://localhost:9091/api/health || exit 1" ]
|
||||
start_period: 15s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
traefik:
|
||||
image: traefik:v3.1
|
||||
container_name: authelia_traefik
|
||||
command:
|
||||
- "--api.insecure=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
networks:
|
||||
- authelia_dev
|
||||
|
||||
ss-atlas:
|
||||
build:
|
||||
context: ./docker/ss-atlas/
|
||||
dockerfile: Dockerfile
|
||||
container_name: atlas_ss_app
|
||||
environment:
|
||||
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-sk_test_placeholder}
|
||||
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-whsec_placeholder}
|
||||
- STRIPE_PRICE_ID=${STRIPE_PRICE_ID:-price_placeholder}
|
||||
- LLDAP_URL=ldap://lldap_lldap:3890
|
||||
- LLDAP_ADMIN_DN=uid=admin,ou=people,dc=a250,dc=ca
|
||||
- LLDAP_ADMIN_PASSWORD=/ETAToLiZPWo6QK171abAUqsa3WDpd9IgneZnTA4zU0=
|
||||
- LLDAP_BASE_DN=dc=a250,dc=ca
|
||||
- DOCKER_HOST=unix:///var/run/docker.sock
|
||||
- APP_URL=http://app.bc.a250.ca
|
||||
- AUTHELIA_URL=http://login.bc.a250.ca
|
||||
- TRAEFIK_DOMAIN=bc.a250.ca
|
||||
- TRAEFIK_NETWORK=authelia_dev
|
||||
- TEMPLATE_PATH=/app/templates
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
networks:
|
||||
- authelia_dev
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.ss-atlas.rule=Host(`app.bc.a250.ca`)"
|
||||
- "traefik.http.routers.ss-atlas.entrypoints=web"
|
||||
- "traefik.http.services.ss-atlas.loadbalancer.server.port=8080"
|
||||
depends_on:
|
||||
lldap:
|
||||
condition: service_healthy
|
||||
authelia:
|
||||
condition: service_healthy
|
||||
|
||||
whoami:
|
||||
image: traefik/whoami
|
||||
container_name: authelia_whoami
|
||||
networks:
|
||||
- authelia_dev
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.whoami.rule=Host(`whoami.bc.a250.ca`)"
|
||||
- "traefik.http.routers.whoami.entrypoints=web"
|
||||
- "traefik.http.routers.whoami.middlewares=authelia-auth@docker"
|
||||
|
||||
networks:
|
||||
authelia_dev:
|
||||
driver: bridge
|
||||
|
|
@ -160,4 +250,4 @@ volumes:
|
|||
authelia_data:
|
||||
driver: local
|
||||
lldap_data:
|
||||
driver: local
|
||||
driver: local
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ services:
|
|||
build:
|
||||
context: ./docker/mariadb/
|
||||
dockerfile: Dockerfile.production
|
||||
image: git.nixc.us/nixius/authelia:production-mariadb
|
||||
image: git.nixc.us/a250/authelia:production-mariadb
|
||||
redis:
|
||||
build:
|
||||
context: ./docker/redis/
|
||||
dockerfile: Dockerfile.production
|
||||
image: git.nixc.us/nixius/authelia:production-redis
|
||||
image: git.nixc.us/a250/authelia:production-redis
|
||||
authelia:
|
||||
build:
|
||||
context: ./docker/authelia/
|
||||
dockerfile: Dockerfile.production
|
||||
image: git.nixc.us/nixius/authelia:production-authelia
|
||||
image: git.nixc.us/a250/authelia:production-authelia
|
||||
|
|
@ -5,14 +5,14 @@ services:
|
|||
build:
|
||||
context: ./docker/mariadb/
|
||||
dockerfile: Dockerfile
|
||||
image: git.nixc.us/nixius/authelia:staging-mariadb
|
||||
image: git.nixc.us/a250/authelia:staging-mariadb
|
||||
redis:
|
||||
build:
|
||||
context: ./docker/redis/
|
||||
dockerfile: Dockerfile
|
||||
image: git.nixc.us/nixius/authelia:staging-redis
|
||||
image: git.nixc.us/a250/authelia:staging-redis
|
||||
authelia:
|
||||
build:
|
||||
context: ./docker/authelia/
|
||||
dockerfile: Dockerfile
|
||||
image: git.nixc.us/nixius/authelia:staging-authelia
|
||||
image: git.nixc.us/a250/authelia:staging-authelia
|
||||
|
|
@ -1 +1 @@
|
|||
FROM git.nixc.us/nixius/authelia:staging-authelia
|
||||
FROM git.nixc.us/a250/authelia:staging-authelia
|
||||
|
|
@ -11,6 +11,9 @@ access_control:
|
|||
# - 10.0.0.0/8
|
||||
|
||||
# # Put WAN Access rules here
|
||||
- domain: "*.{{ env "TRAEFIK_DOMAIN" }}"
|
||||
policy: bypass
|
||||
|
||||
# - domain: {{ env "TRAEFIK_DOMAIN" }}
|
||||
# resources:
|
||||
# - "^/.well-known([/?].*)?$"
|
||||
|
|
@ -23,49 +26,77 @@ access_control:
|
|||
# - domain: headscale.{{ env "TRAEFIK_DOMAIN" }}
|
||||
# policy: bypass
|
||||
|
||||
# Customer stacks: authenticated subscribers with one_factor
|
||||
- domain_regex: '^[a-z0-9-]+\.{{ env "TRAEFIK_DOMAIN" }}$'
|
||||
subject:
|
||||
- "group:customers"
|
||||
policy: one_factor
|
||||
|
||||
# ss-atlas app public routes (landing, webhook)
|
||||
- domain: 'app.{{ env "TRAEFIK_DOMAIN" }}'
|
||||
policy: bypass
|
||||
resources:
|
||||
- '^/$'
|
||||
- '^/subscribe$'
|
||||
- '^/success(\?.*)?$'
|
||||
- '^/webhook/stripe$'
|
||||
- '^/health$'
|
||||
|
||||
# ss-atlas activate requires any authenticated user (not yet in customers group)
|
||||
- domain: 'app.{{ env "TRAEFIK_DOMAIN" }}'
|
||||
resources:
|
||||
- '^/activate$'
|
||||
policy: one_factor
|
||||
|
||||
# ss-atlas dashboard requires auth
|
||||
- domain: 'app.{{ env "TRAEFIK_DOMAIN" }}'
|
||||
subject:
|
||||
- "group:customers"
|
||||
policy: one_factor
|
||||
|
||||
# Admin services require two-factor authentication
|
||||
- domain:
|
||||
- "portainer.nixc.us"
|
||||
- "login.nixc.us"
|
||||
- "portainer.a250.ca"
|
||||
- "login.a250.ca"
|
||||
- "git.nixc.us"
|
||||
subject:
|
||||
- "group:admins"
|
||||
policy: two_factor
|
||||
|
||||
# General admin access (less sensitive services)
|
||||
- domain: "*.nixc.us"
|
||||
- domain: "*.a250.ca"
|
||||
subject:
|
||||
- "group:admins"
|
||||
# - "group:dev"
|
||||
policy: one_factor
|
||||
# traefik monitor
|
||||
- domain:
|
||||
- "monitor-ertest.nixc.us"
|
||||
- "monitor-ertest.a250.ca"
|
||||
subject:
|
||||
- "group:monitor-ertest"
|
||||
policy: one_factor
|
||||
# guacamole
|
||||
- domain:
|
||||
- "guac.nixc.us"
|
||||
- "guac.a250.ca"
|
||||
subject:
|
||||
- "group:guac"
|
||||
policy: one_factor
|
||||
# uptime-kuma
|
||||
- domain:
|
||||
- "uptime.nixc.us"
|
||||
- "uptime.a250.ca"
|
||||
subject:
|
||||
- "group:uptime-kuma"
|
||||
policy: one_factor
|
||||
# Filebrowser and Bypass
|
||||
- domain:
|
||||
- "fb.nixc.us"
|
||||
- "fbi.nixc.us"
|
||||
- "fb.a250.ca"
|
||||
- "fbi.a250.ca"
|
||||
subject:
|
||||
- "group:admins"
|
||||
policy: one_factor
|
||||
- domain:
|
||||
- "fb.nixc.us"
|
||||
- "fbi.nixc.us"
|
||||
- "fb.a250.ca"
|
||||
- "fbi.a250.ca"
|
||||
policy: bypass
|
||||
resources:
|
||||
- '^/api/(.*)?$'
|
||||
|
|
@ -73,54 +104,54 @@ access_control:
|
|||
- '^/static/(.*)?$'
|
||||
## Transfer.sh
|
||||
- domain:
|
||||
- "tx.nixc.us"
|
||||
- "tx.a250.ca"
|
||||
subject:
|
||||
- "group:transfer"
|
||||
policy: one_factor
|
||||
## Firefox
|
||||
- domain:
|
||||
- "ff.nixc.us"
|
||||
- "ff.a250.ca"
|
||||
subject:
|
||||
- "group:firefox"
|
||||
policy: one_factor
|
||||
- domain:
|
||||
- "oracle.nixc.us"
|
||||
- "oracle.a250.ca"
|
||||
subject:
|
||||
- "group:oracle"
|
||||
policy: one_factor
|
||||
## Stash
|
||||
- domain:
|
||||
- "fb.nixc.us"
|
||||
- "fb.a250.ca"
|
||||
subject:
|
||||
- "group:fansdb"
|
||||
policy: one_factor
|
||||
# Filebrowser and Bypass
|
||||
- domain:
|
||||
- "fb-stash.nixc.us"
|
||||
- "fb-stash.a250.ca"
|
||||
subject:
|
||||
- "group:stash_admin"
|
||||
policy: one_factor
|
||||
# Graylog access (sensitive logs require two-factor)
|
||||
- domain:
|
||||
- "log.nixc.us"
|
||||
- "log.a250.ca"
|
||||
subject:
|
||||
- "group:graylog"
|
||||
policy: two_factor
|
||||
# whisper access
|
||||
- domain:
|
||||
- "whisper.nixc.us"
|
||||
- "whisper.a250.ca"
|
||||
subject:
|
||||
- "group:kwlug"
|
||||
policy: one_factor
|
||||
# whisper access
|
||||
- domain:
|
||||
- "marketing-browser.nixc.us"
|
||||
- "marketing-browser.a250.ca"
|
||||
subject:
|
||||
- "group:mrc"
|
||||
policy: one_factor
|
||||
# scanner access
|
||||
- domain:
|
||||
- "scanner.oid.nixc.us"
|
||||
- "scanner.oid.a250.ca"
|
||||
subject:
|
||||
- "group:mrc"
|
||||
policy: one_factor
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
notifier:
|
||||
filesystem:
|
||||
filename: /data/notification.txt
|
||||
|
|
@ -20,7 +20,7 @@ totp:
|
|||
webauthn:
|
||||
disable: false
|
||||
enable_passkey_login: true
|
||||
display_name: Authelia
|
||||
display_name: a250.ca
|
||||
attestation_conveyance_preference: indirect
|
||||
timeout: 60s
|
||||
selection_criteria:
|
||||
|
|
@ -49,14 +49,6 @@ storage:
|
|||
password: authelia
|
||||
timeout: 5s
|
||||
|
||||
notifier:
|
||||
smtp:
|
||||
address: submissions://box.p.nixc.us
|
||||
username: {{ env "X_AUTHELIA_EMAIL" }}
|
||||
password: {{ secret "/run/secrets/NOTIFIER_SMTP_PASSWORD" }}
|
||||
sender: "{{ env "X_AUTHELIA_SITE_NAME" }} <{{ env "X_AUTHELIA_EMAIL" }}>"
|
||||
subject: "[Authelia] {title}"
|
||||
|
||||
session:
|
||||
secret: {{ secret "/run/secrets/SESSION_SECRET" }}
|
||||
name: authelia_session
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
FROM git.nixc.us/nixius/authelia:staging-mariadb
|
||||
FROM git.nixc.us/a250/authelia:staging-mariadb
|
||||
|
|
@ -1 +1 @@
|
|||
FROM git.nixc.us/nixius/authelia:staging-redis
|
||||
FROM git.nixc.us/a250/authelia:staging-redis
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
FROM golang:1.23-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /ss-atlas ./cmd/
|
||||
|
||||
FROM alpine:3.21
|
||||
RUN apk add --no-cache ca-certificates
|
||||
WORKDIR /app
|
||||
COPY --from=builder /ss-atlas /app/ss-atlas
|
||||
COPY templates/ /app/templates/
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/app/ss-atlas"]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.PHONY: test build
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
build:
|
||||
go build -o ss-atlas ./cmd/main.go
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.nixc.us/a250/ss-atlas/internal/config"
|
||||
"git.nixc.us/a250/ss-atlas/internal/handlers"
|
||||
"git.nixc.us/a250/ss-atlas/internal/ldap"
|
||||
ssstripe "git.nixc.us/a250/ss-atlas/internal/stripe"
|
||||
"git.nixc.us/a250/ss-atlas/internal/swarm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
stripeClient := ssstripe.New(cfg)
|
||||
ldapClient := ldap.New(cfg)
|
||||
swarmClient := swarm.New(cfg)
|
||||
|
||||
router := handlers.NewRouter(cfg, stripeClient, ldapClient, swarmClient)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.Port,
|
||||
Handler: router,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("ss-atlas listening on :%s", cfg.Port)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("shutting down...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("forced shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
module git.nixc.us/a250/ss-atlas
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-ldap/ldap/v3 v3.4.10
|
||||
github.com/stripe/stripe-go/v84 v84.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
)
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
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/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
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-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stripe/stripe-go/v84 v84.4.0 h1:JMQMqb+mhW6tns+eYA3G5SZiaoD2ULwN0lZ+kNjWAsY=
|
||||
github.com/stripe/stripe-go/v84 v84.4.0/go.mod h1:Z4gcKw1zl4geDG2+cjpSaJES9jaohGX6n7FP8/kHIqw=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package checks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const maxLines = 400
|
||||
|
||||
// TestNoFileExceedsMaxLines ensures no project file exceeds the line limit.
|
||||
// This keeps the codebase modular and maintainable.
|
||||
func TestNoFileExceedsMaxLines(t *testing.T) {
|
||||
_, thisFile, _, _ := runtime.Caller(0)
|
||||
root := filepath.Join(filepath.Dir(thisFile), "..", "..")
|
||||
|
||||
exts := map[string]bool{
|
||||
".go": true, ".yml": true, ".yaml": true, ".html": true,
|
||||
".sh": true, ".md": true,
|
||||
}
|
||||
|
||||
var over []string
|
||||
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
name := info.Name()
|
||||
if name == "vendor" || name == "node_modules" || name == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !exts[strings.ToLower(filepath.Ext(path))] {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
lines := strings.Count(string(data), "\n")
|
||||
if strings.HasSuffix(string(data), "\n") {
|
||||
// last line has newline
|
||||
} else if len(data) > 0 {
|
||||
lines++
|
||||
}
|
||||
if lines > maxLines {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
over = append(over, rel+":"+strconv.Itoa(lines))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if len(over) > 0 {
|
||||
t.Errorf("files exceed %d lines:\n%s", maxLines, strings.Join(over, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
AppURL string
|
||||
AutheliaURL string
|
||||
StripeSecretKey string
|
||||
StripeWebhookSecret string
|
||||
StripePriceID string
|
||||
LDAPUrl string
|
||||
LDAPAdminDN string
|
||||
LDAPAdminPassword string
|
||||
LDAPBaseDN string
|
||||
DockerHost string
|
||||
TraefikDomain string
|
||||
TraefikNetwork string
|
||||
TemplatePath string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Port: envOrDefault("PORT", "8080"),
|
||||
AppURL: envOrDefault("APP_URL", "http://app.bc.a250.ca"),
|
||||
AutheliaURL: envOrDefault("AUTHELIA_URL", "http://login.bc.a250.ca"),
|
||||
StripeSecretKey: envOrDefault("STRIPE_SECRET_KEY", ""),
|
||||
StripeWebhookSecret: envOrDefault("STRIPE_WEBHOOK_SECRET", ""),
|
||||
StripePriceID: envOrDefault("STRIPE_PRICE_ID", ""),
|
||||
LDAPUrl: envOrDefault("LLDAP_URL", "ldap://lldap_lldap:3890"),
|
||||
LDAPAdminDN: envOrDefault("LLDAP_ADMIN_DN", "uid=admin,ou=people,dc=a250,dc=ca"),
|
||||
LDAPAdminPassword: envOrDefault("LLDAP_ADMIN_PASSWORD", ""),
|
||||
LDAPBaseDN: envOrDefault("LLDAP_BASE_DN", "dc=a250,dc=ca"),
|
||||
DockerHost: envOrDefault("DOCKER_HOST", "unix:///var/run/docker.sock"),
|
||||
TraefikDomain: envOrDefault("TRAEFIK_DOMAIN", "bc.a250.ca"),
|
||||
TraefikNetwork: envOrDefault("TRAEFIK_NETWORK", "authelia_dev"),
|
||||
TemplatePath: envOrDefault("TEMPLATE_PATH", "/app/templates"),
|
||||
}
|
||||
}
|
||||
|
||||
func envOrDefault(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnvOrDefault(t *testing.T) {
|
||||
key := "SSATLAS_TEST_ENV_VAR"
|
||||
defer os.Unsetenv(key)
|
||||
|
||||
if got := envOrDefault(key, "default"); got != "default" {
|
||||
t.Errorf("unset env: got %q, want default", got)
|
||||
}
|
||||
os.Setenv(key, "custom")
|
||||
if got := envOrDefault(key, "default"); got != "custom" {
|
||||
t.Errorf("set env: got %q, want custom", got)
|
||||
}
|
||||
os.Setenv(key, "")
|
||||
if got := envOrDefault(key, "default"); got != "default" {
|
||||
t.Errorf("empty env: got %q, want default", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDefaults(t *testing.T) {
|
||||
// Clear env vars that Load uses
|
||||
envKeys := []string{
|
||||
"PORT", "APP_URL", "AUTHELIA_URL", "STRIPE_SECRET_KEY",
|
||||
"STRIPE_WEBHOOK_SECRET", "STRIPE_PRICE_ID", "LLDAP_URL",
|
||||
"LLDAP_ADMIN_DN", "LLDAP_ADMIN_PASSWORD", "LLDAP_BASE_DN",
|
||||
"DOCKER_HOST", "TRAEFIK_DOMAIN", "TRAEFIK_NETWORK", "TEMPLATE_PATH",
|
||||
}
|
||||
for _, k := range envKeys {
|
||||
os.Unsetenv(k)
|
||||
}
|
||||
defer func() {
|
||||
for _, k := range envKeys {
|
||||
os.Unsetenv(k)
|
||||
}
|
||||
}()
|
||||
|
||||
cfg := Load()
|
||||
|
||||
tests := []struct {
|
||||
field string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{"Port", cfg.Port, "8080"},
|
||||
{"AppURL", cfg.AppURL, "http://app.bc.a250.ca"},
|
||||
{"AutheliaURL", cfg.AutheliaURL, "http://login.bc.a250.ca"},
|
||||
{"LDAPUrl", cfg.LDAPUrl, "ldap://lldap_lldap:3890"},
|
||||
{"LDAPAdminDN", cfg.LDAPAdminDN, "uid=admin,ou=people,dc=a250,dc=ca"},
|
||||
{"LDAPBaseDN", cfg.LDAPBaseDN, "dc=a250,dc=ca"},
|
||||
{"DockerHost", cfg.DockerHost, "unix:///var/run/docker.sock"},
|
||||
{"TraefikDomain", cfg.TraefikDomain, "bc.a250.ca"},
|
||||
{"TraefikNetwork", cfg.TraefikNetwork, "authelia_dev"},
|
||||
{"TemplatePath", cfg.TemplatePath, "/app/templates"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.got != tt.want {
|
||||
t.Errorf("Load().%s = %q, want %q", tt.field, tt.got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFromEnv(t *testing.T) {
|
||||
os.Setenv("PORT", "9000")
|
||||
os.Setenv("TEMPLATE_PATH", "/custom/templates")
|
||||
defer os.Unsetenv("PORT")
|
||||
defer os.Unsetenv("TEMPLATE_PATH")
|
||||
|
||||
cfg := Load()
|
||||
if cfg.Port != "9000" {
|
||||
t.Errorf("Port = %q, want 9000", cfg.Port)
|
||||
}
|
||||
if cfg.TemplatePath != "/custom/templates" {
|
||||
t.Errorf("TemplatePath = %q, want /custom/templates", cfg.TemplatePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTemplatePathRelative(t *testing.T) {
|
||||
wd, _ := os.Getwd()
|
||||
relPath := filepath.Join(wd, "..", "..", "templates")
|
||||
os.Setenv("TEMPLATE_PATH", relPath)
|
||||
defer os.Unsetenv("TEMPLATE_PATH")
|
||||
|
||||
cfg := Load()
|
||||
if cfg.TemplatePath != relPath {
|
||||
t.Errorf("TemplatePath = %q, want %q", cfg.TemplatePath, relPath)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (a *App) handleActivateGet(w http.ResponseWriter, r *http.Request) {
|
||||
remoteUser := r.Header.Get("Remote-User")
|
||||
if remoteUser == "" {
|
||||
data := map[string]any{
|
||||
"AutheliaURL": a.cfg.AutheliaURL,
|
||||
"AppURL": a.cfg.AppURL,
|
||||
"NeedLogin": true,
|
||||
}
|
||||
a.tmpl.ExecuteTemplate(w, "activate.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
inGroup, _ := a.ldap.IsInGroup(remoteUser, "customers")
|
||||
if inGroup {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"User": remoteUser,
|
||||
"AppURL": a.cfg.AppURL,
|
||||
"Ready": true,
|
||||
}
|
||||
if err := a.tmpl.ExecuteTemplate(w, "activate.html", data); err != nil {
|
||||
log.Printf("template error: %v", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleActivatePost(w http.ResponseWriter, r *http.Request) {
|
||||
remoteUser := r.Header.Get("Remote-User")
|
||||
if remoteUser == "" {
|
||||
http.Error(w, "not authenticated", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
inGroup, _ := a.ldap.IsInGroup(remoteUser, "customers")
|
||||
if inGroup {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.ldap.AddToGroup(remoteUser, "customers"); err != nil {
|
||||
log.Printf("activate: group add failed for %s: %v", remoteUser, err)
|
||||
http.Error(w, "activation failed, contact support", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
stackName := fmt.Sprintf("customer-%s", remoteUser)
|
||||
if err := a.swarm.DeployStack(stackName, remoteUser, a.cfg.TraefikDomain); err != nil {
|
||||
log.Printf("activate: stack deploy failed for %s: %v", remoteUser, err)
|
||||
}
|
||||
|
||||
log.Printf("activated user %s: group=customers stack=%s", remoteUser, stackName)
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
remoteUser := r.Header.Get("Remote-User")
|
||||
remoteEmail := r.Header.Get("Remote-Email")
|
||||
remoteGroups := r.Header.Get("Remote-Groups")
|
||||
|
||||
data := map[string]any{
|
||||
"AppURL": a.cfg.AppURL,
|
||||
"AutheliaURL": a.cfg.AutheliaURL,
|
||||
"User": remoteUser,
|
||||
"Email": remoteEmail,
|
||||
"Groups": remoteGroups,
|
||||
"Domain": a.cfg.TraefikDomain,
|
||||
"IsSubscribed": remoteGroups != "" && contains(remoteGroups, "customers"),
|
||||
}
|
||||
|
||||
if err := a.tmpl.ExecuteTemplate(w, "dashboard.html", data); err != nil {
|
||||
log.Printf("template error: %v", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && searchString(s, substr)
|
||||
}
|
||||
|
||||
func searchString(s, sub string) bool {
|
||||
for i := 0; i <= len(s)-len(sub); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"git.nixc.us/a250/ss-atlas/internal/config"
|
||||
)
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
sub string
|
||||
want bool
|
||||
}{
|
||||
{"customers,admins", "customers", true},
|
||||
{"admins,customers", "customers", true},
|
||||
{"customers", "customers", true},
|
||||
{"admins,users", "customers", false},
|
||||
{"", "customers", false},
|
||||
{"cus", "customers", false},
|
||||
{"customersx", "customers", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := contains(tt.s, tt.sub); got != tt.want {
|
||||
t.Errorf("contains(%q, %q) = %v, want %v", tt.s, tt.sub, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchString(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
sub string
|
||||
want bool
|
||||
}{
|
||||
{"customers,admins", "customers", true},
|
||||
{"admins", "admins", true},
|
||||
{"a", "a", true},
|
||||
{"abc", "b", true},
|
||||
{"abc", "d", false},
|
||||
{"", "a", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := searchString(tt.s, tt.sub); got != tt.want {
|
||||
t.Errorf("searchString(%q, %q) = %v, want %v", tt.s, tt.sub, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeUsername(t *testing.T) {
|
||||
tests := []struct {
|
||||
email string
|
||||
want string
|
||||
}{
|
||||
{"user@example.com", "user"},
|
||||
{"User.Name@domain.com", "user-name"},
|
||||
{"user_name@domain.com", "user_name"},
|
||||
{"user123@domain.com", "user123"},
|
||||
{"UPPER@domain.com", "upper"},
|
||||
{"a.b.c@x.com", "a-b-c"},
|
||||
{"spécial@x.com", "sp-cial"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := sanitizeUsername(tt.email); got != tt.want {
|
||||
t.Errorf("sanitizeUsername(%q) = %q, want %q", tt.email, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthRoute(t *testing.T) {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
rootDir := filepath.Join(filepath.Dir(filename), "..", "..")
|
||||
templatePath := filepath.Join(rootDir, "templates")
|
||||
|
||||
cfg := &config.Config{
|
||||
Port: "8080",
|
||||
TemplatePath: templatePath,
|
||||
}
|
||||
router := NewRouter(cfg, nil, nil, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("GET /health status = %d, want 200", rec.Code)
|
||||
}
|
||||
if body := rec.Body.String(); body != "ok" {
|
||||
t.Errorf("GET /health body = %q, want ok", body)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"git.nixc.us/a250/ss-atlas/internal/config"
|
||||
"git.nixc.us/a250/ss-atlas/internal/ldap"
|
||||
ssstripe "git.nixc.us/a250/ss-atlas/internal/stripe"
|
||||
"git.nixc.us/a250/ss-atlas/internal/swarm"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg *config.Config
|
||||
stripe *ssstripe.Client
|
||||
ldap *ldap.Client
|
||||
swarm *swarm.Client
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
func NewRouter(cfg *config.Config, sc *ssstripe.Client, lc *ldap.Client, sw *swarm.Client) http.Handler {
|
||||
tmplPattern := filepath.Join(cfg.TemplatePath, "pages", "*.html")
|
||||
tmpl := template.Must(template.ParseGlob(tmplPattern))
|
||||
|
||||
app := &App{
|
||||
cfg: cfg,
|
||||
stripe: sc,
|
||||
ldap: lc,
|
||||
swarm: sw,
|
||||
tmpl: tmpl,
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.RealIP)
|
||||
|
||||
r.Get("/", app.handleLanding)
|
||||
r.Get("/success", app.handleSuccess)
|
||||
r.Get("/activate", app.handleActivateGet)
|
||||
r.Post("/activate", app.handleActivatePost)
|
||||
r.Get("/dashboard", app.handleDashboard)
|
||||
r.Post("/subscribe", app.handleCreateCheckout)
|
||||
r.Post("/portal", app.handlePortal)
|
||||
r.Post("/webhook/stripe", app.handleWebhook)
|
||||
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (a *App) handleLanding(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]any{
|
||||
"AppURL": a.cfg.AppURL,
|
||||
}
|
||||
if err := a.tmpl.ExecuteTemplate(w, "landing.html", data); err != nil {
|
||||
log.Printf("template error: %v", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleCreateCheckout(w http.ResponseWriter, r *http.Request) {
|
||||
email := r.FormValue("email")
|
||||
if email == "" {
|
||||
http.Error(w, "email required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := a.stripe.CreateCheckoutSession(email)
|
||||
if err != nil {
|
||||
log.Printf("stripe checkout error: %v", err)
|
||||
http.Error(w, "failed to create checkout", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, sess.URL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||
sessionID := r.URL.Query().Get("session_id")
|
||||
if sessionID == "" {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := a.stripe.GetCheckoutSession(sessionID)
|
||||
if err != nil {
|
||||
log.Printf("stripe get session error: %v", err)
|
||||
http.Error(w, "could not verify payment", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if sess.PaymentStatus != "paid" {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
email := sess.CustomerDetails.Email
|
||||
customerID := sess.Customer.ID
|
||||
username := sanitizeUsername(email)
|
||||
|
||||
// Create the LLDAP user but do NOT add to group or deploy stack yet.
|
||||
// That happens on /activate after the user has set their own password.
|
||||
result, err := a.ldap.ProvisionUser(username, email, customerID)
|
||||
if err != nil {
|
||||
log.Printf("ldap provision failed for %s: %v", email, err)
|
||||
http.Error(w, "account creation failed, contact support", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"Username": result.Username,
|
||||
"Password": result.Password,
|
||||
"IsNew": result.IsNew,
|
||||
"Email": email,
|
||||
"LoginURL": a.cfg.AutheliaURL,
|
||||
"ResetURL": a.cfg.AutheliaURL + "/#/reset-password/step1",
|
||||
"ActivateURL": a.cfg.AppURL + "/activate",
|
||||
"DashboardURL": a.cfg.AppURL + "/dashboard",
|
||||
}
|
||||
|
||||
if err := a.tmpl.ExecuteTemplate(w, "welcome.html", data); err != nil {
|
||||
log.Printf("template error: %v", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handlePortal(w http.ResponseWriter, r *http.Request) {
|
||||
customerID := r.FormValue("customer_id")
|
||||
if customerID == "" {
|
||||
http.Error(w, "customer_id required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := a.stripe.CreatePortalSession(customerID)
|
||||
if err != nil {
|
||||
log.Printf("stripe portal error: %v", err)
|
||||
http.Error(w, "failed to create portal session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, sess.URL, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func sanitizeUsername(email string) string {
|
||||
parts := strings.SplitN(email, "@", 2)
|
||||
name := strings.ToLower(parts[0])
|
||||
name = strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||||
return r
|
||||
}
|
||||
return '-'
|
||||
}, name)
|
||||
return name
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
stripego "github.com/stripe/stripe-go/v84"
|
||||
"github.com/stripe/stripe-go/v84/webhook"
|
||||
)
|
||||
|
||||
const maxWebhookPayload = 65536
|
||||
|
||||
func (a *App) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, maxWebhookPayload))
|
||||
if err != nil {
|
||||
http.Error(w, "read error", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sig := r.Header.Get("Stripe-Signature")
|
||||
event, err := webhook.ConstructEvent(body, sig, a.stripe.WebhookSecret())
|
||||
if err != nil {
|
||||
log.Printf("webhook signature verification failed: %v", err)
|
||||
http.Error(w, "invalid signature", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
switch event.Type {
|
||||
case "checkout.session.completed":
|
||||
a.onCheckoutCompleted(event)
|
||||
case "customer.subscription.deleted":
|
||||
a.onSubscriptionDeleted(event)
|
||||
case "customer.subscription.updated":
|
||||
a.onSubscriptionUpdated(event)
|
||||
default:
|
||||
log.Printf("unhandled webhook event: %s", event.Type)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// Reconciliation: ensures LLDAP user exists. Group + stack are handled by /activate.
|
||||
func (a *App) onCheckoutCompleted(event stripego.Event) {
|
||||
var sess stripego.CheckoutSession
|
||||
if err := json.Unmarshal(event.Data.Raw, &sess); err != nil {
|
||||
log.Printf("checkout unmarshal error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
email := sess.CustomerDetails.Email
|
||||
customerID := sess.Customer.ID
|
||||
username := sanitizeUsername(email)
|
||||
|
||||
log.Printf("webhook: checkout completed email=%s customer=%s", email, customerID)
|
||||
|
||||
if err := a.ldap.EnsureUser(username, email, customerID); err != nil {
|
||||
log.Printf("webhook: ldap ensure user failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) onSubscriptionDeleted(event stripego.Event) {
|
||||
var sub stripego.Subscription
|
||||
if err := json.Unmarshal(event.Data.Raw, &sub); err != nil {
|
||||
log.Printf("subscription unmarshal error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
customerID := sub.Customer.ID
|
||||
log.Printf("subscription deleted for customer %s", customerID)
|
||||
|
||||
username, err := a.ldap.FindUserByDescription(customerID)
|
||||
if err != nil {
|
||||
log.Printf("could not find user for customer %s: %v", customerID, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.ldap.RemoveFromGroup(username, "customers"); err != nil {
|
||||
log.Printf("ldap group remove failed: %v", err)
|
||||
}
|
||||
|
||||
stackName := "customer-" + username
|
||||
if err := a.swarm.RemoveStack(stackName); err != nil {
|
||||
log.Printf("stack remove failed for %s: %v", stackName, err)
|
||||
}
|
||||
|
||||
log.Printf("deprovisioned stack for customer %s (%s)", customerID, username)
|
||||
}
|
||||
|
||||
func (a *App) onSubscriptionUpdated(event stripego.Event) {
|
||||
var sub stripego.Subscription
|
||||
if err := json.Unmarshal(event.Data.Raw, &sub); err != nil {
|
||||
log.Printf("subscription update unmarshal error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if sub.Status == stripego.SubscriptionStatusCanceled ||
|
||||
sub.Status == stripego.SubscriptionStatusUnpaid {
|
||||
log.Printf("subscription %s status=%s, will be cleaned up on deletion", sub.ID, sub.Status)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.nixc.us/a250/ss-atlas/internal/config"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
type ProvisionResult struct {
|
||||
Username string
|
||||
Password string
|
||||
IsNew bool
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *Client {
|
||||
return &Client{cfg: cfg}
|
||||
}
|
||||
|
||||
func (c *Client) connect() (*goldap.Conn, error) {
|
||||
conn, err := goldap.DialURL(c.cfg.LDAPUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ldap dial: %w", err)
|
||||
}
|
||||
if err := conn.Bind(c.cfg.LDAPAdminDN, c.cfg.LDAPAdminPassword); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("ldap bind: %w", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Client) ProvisionUser(username, email, stripeCustomerID string) (*ProvisionResult, error) {
|
||||
conn, err := c.connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
exists, err := c.userExists(conn, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
log.Printf("ldap user %s already exists", username)
|
||||
return &ProvisionResult{Username: username, IsNew: false}, nil
|
||||
}
|
||||
|
||||
password := generatePassword()
|
||||
userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN)
|
||||
|
||||
addReq := goldap.NewAddRequest(userDN, nil)
|
||||
addReq.Attribute("objectClass", []string{"inetOrgPerson"})
|
||||
addReq.Attribute("cn", []string{username})
|
||||
addReq.Attribute("sn", []string{username})
|
||||
addReq.Attribute("uid", []string{username})
|
||||
addReq.Attribute("mail", []string{email})
|
||||
addReq.Attribute("userPassword", []string{password})
|
||||
addReq.Attribute("description", []string{stripeCustomerID})
|
||||
|
||||
if err := conn.Add(addReq); err != nil {
|
||||
return nil, fmt.Errorf("ldap add user %s: %w", username, err)
|
||||
}
|
||||
|
||||
log.Printf("created ldap user %s (%s)", username, email)
|
||||
return &ProvisionResult{Username: username, Password: password, IsNew: true}, nil
|
||||
}
|
||||
|
||||
func (c *Client) EnsureUser(username, email, stripeCustomerID string) error {
|
||||
_, err := c.ProvisionUser(username, email, stripeCustomerID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) AddToGroup(username, groupName string) error {
|
||||
conn, err := c.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
groupDN := fmt.Sprintf("cn=%s,ou=groups,%s", groupName, c.cfg.LDAPBaseDN)
|
||||
userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN)
|
||||
|
||||
modReq := goldap.NewModifyRequest(groupDN, nil)
|
||||
modReq.Add("member", []string{userDN})
|
||||
|
||||
if err := conn.Modify(modReq); err != nil {
|
||||
return fmt.Errorf("ldap add %s to group %s: %w", username, groupName, err)
|
||||
}
|
||||
|
||||
log.Printf("added %s to group %s", username, groupName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) RemoveFromGroup(username, groupName string) error {
|
||||
conn, err := c.connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
groupDN := fmt.Sprintf("cn=%s,ou=groups,%s", groupName, c.cfg.LDAPBaseDN)
|
||||
userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN)
|
||||
|
||||
modReq := goldap.NewModifyRequest(groupDN, nil)
|
||||
modReq.Delete("member", []string{userDN})
|
||||
|
||||
if err := conn.Modify(modReq); err != nil {
|
||||
return fmt.Errorf("ldap remove %s from group %s: %w", username, groupName, err)
|
||||
}
|
||||
|
||||
log.Printf("removed %s from group %s", username, groupName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) IsInGroup(username, groupName string) (bool, error) {
|
||||
conn, err := c.connect()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
groupDN := fmt.Sprintf("cn=%s,ou=groups,%s", groupName, c.cfg.LDAPBaseDN)
|
||||
userDN := fmt.Sprintf("uid=%s,ou=people,%s", username, c.cfg.LDAPBaseDN)
|
||||
|
||||
searchReq := goldap.NewSearchRequest(
|
||||
groupDN,
|
||||
goldap.ScopeBaseObject, goldap.NeverDerefAliases, 1, 0, false,
|
||||
fmt.Sprintf("(member=%s)", goldap.EscapeFilter(userDN)),
|
||||
[]string{"cn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
result, err := conn.Search(searchReq)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return len(result.Entries) > 0, nil
|
||||
}
|
||||
|
||||
func (c *Client) FindUserByDescription(stripeCustomerID string) (string, error) {
|
||||
conn, err := c.connect()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
searchReq := goldap.NewSearchRequest(
|
||||
fmt.Sprintf("ou=people,%s", c.cfg.LDAPBaseDN),
|
||||
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 1, 0, false,
|
||||
fmt.Sprintf("(description=%s)", goldap.EscapeFilter(stripeCustomerID)),
|
||||
[]string{"uid"},
|
||||
nil,
|
||||
)
|
||||
|
||||
result, err := conn.Search(searchReq)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ldap search by description: %w", err)
|
||||
}
|
||||
|
||||
if len(result.Entries) == 0 {
|
||||
return "", fmt.Errorf("no user found with stripe customer %s", stripeCustomerID)
|
||||
}
|
||||
|
||||
return result.Entries[0].GetAttributeValue("uid"), nil
|
||||
}
|
||||
|
||||
func (c *Client) userExists(conn *goldap.Conn, username string) (bool, error) {
|
||||
searchReq := goldap.NewSearchRequest(
|
||||
fmt.Sprintf("ou=people,%s", c.cfg.LDAPBaseDN),
|
||||
goldap.ScopeWholeSubtree, goldap.NeverDerefAliases, 1, 0, false,
|
||||
fmt.Sprintf("(uid=%s)", goldap.EscapeFilter(username)),
|
||||
[]string{"uid"},
|
||||
nil,
|
||||
)
|
||||
|
||||
result, err := conn.Search(searchReq)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("ldap search: %w", err)
|
||||
}
|
||||
|
||||
return len(result.Entries) > 0, nil
|
||||
}
|
||||
|
||||
func generatePassword() string {
|
||||
b := make([]byte, 18)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic("crypto/rand failed: " + err.Error())
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package stripe
|
||||
|
||||
import (
|
||||
"git.nixc.us/a250/ss-atlas/internal/config"
|
||||
stripego "github.com/stripe/stripe-go/v84"
|
||||
portalsession "github.com/stripe/stripe-go/v84/billingportal/session"
|
||||
checkoutsession "github.com/stripe/stripe-go/v84/checkout/session"
|
||||
"github.com/stripe/stripe-go/v84/subscription"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *Client {
|
||||
stripego.Key = cfg.StripeSecretKey
|
||||
return &Client{cfg: cfg}
|
||||
}
|
||||
|
||||
func (c *Client) CreateCheckoutSession(email string) (*stripego.CheckoutSession, error) {
|
||||
params := &stripego.CheckoutSessionParams{
|
||||
Mode: stripego.String(string(stripego.CheckoutSessionModeSubscription)),
|
||||
LineItems: []*stripego.CheckoutSessionLineItemParams{
|
||||
{
|
||||
Price: stripego.String(c.cfg.StripePriceID),
|
||||
Quantity: stripego.Int64(1),
|
||||
},
|
||||
},
|
||||
CustomerEmail: stripego.String(email),
|
||||
SuccessURL: stripego.String(c.cfg.AppURL + "/success?session_id={CHECKOUT_SESSION_ID}"),
|
||||
CancelURL: stripego.String(c.cfg.AppURL + "/"),
|
||||
}
|
||||
return checkoutsession.New(params)
|
||||
}
|
||||
|
||||
func (c *Client) CreatePortalSession(customerID string) (*stripego.BillingPortalSession, error) {
|
||||
params := &stripego.BillingPortalSessionParams{
|
||||
Customer: stripego.String(customerID),
|
||||
ReturnURL: stripego.String(c.cfg.AppURL + "/dashboard"),
|
||||
}
|
||||
return portalsession.New(params)
|
||||
}
|
||||
|
||||
func (c *Client) GetCheckoutSession(sessionID string) (*stripego.CheckoutSession, error) {
|
||||
params := &stripego.CheckoutSessionParams{}
|
||||
params.AddExpand("customer")
|
||||
params.AddExpand("subscription")
|
||||
return checkoutsession.Get(sessionID, params)
|
||||
}
|
||||
|
||||
func (c *Client) GetSubscription(subID string) (*stripego.Subscription, error) {
|
||||
return subscription.Get(subID, nil)
|
||||
}
|
||||
|
||||
func (c *Client) WebhookSecret() string {
|
||||
return c.cfg.StripeWebhookSecret
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"git.nixc.us/a250/ss-atlas/internal/config"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *Client {
|
||||
return &Client{cfg: cfg}
|
||||
}
|
||||
|
||||
func (c *Client) DeployStack(stackName, username, domain string) error {
|
||||
tmplPath := filepath.Join(c.cfg.TemplatePath, "stack-template.yml")
|
||||
tmplBytes, err := os.ReadFile(tmplPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read stack template: %w", err)
|
||||
}
|
||||
|
||||
t, err := template.New("stack").Parse(string(tmplBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse stack template: %w", err)
|
||||
}
|
||||
|
||||
data := map[string]string{
|
||||
"ID": username,
|
||||
"Subdomain": username,
|
||||
"Domain": domain,
|
||||
"TraefikNetwork": c.cfg.TraefikNetwork,
|
||||
}
|
||||
|
||||
var rendered bytes.Buffer
|
||||
if err := t.Execute(&rendered, data); err != nil {
|
||||
return fmt.Errorf("render stack template: %w", err)
|
||||
}
|
||||
|
||||
tmpFile := filepath.Join(os.TempDir(), stackName+".yml")
|
||||
if err := os.WriteFile(tmpFile, rendered.Bytes(), 0600); err != nil {
|
||||
return fmt.Errorf("write temp stack file: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpFile)
|
||||
|
||||
cmd := exec.Command("docker", "stack", "deploy",
|
||||
"-c", tmpFile,
|
||||
stackName,
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("docker stack deploy: %s: %w", strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
|
||||
log.Printf("deployed stack %s: %s", stackName, strings.TrimSpace(string(output)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) RemoveStack(stackName string) error {
|
||||
cmd := exec.Command("docker", "stack", "rm", stackName)
|
||||
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("docker stack rm: %s: %w", strings.TrimSpace(string(output)), err)
|
||||
}
|
||||
|
||||
log.Printf("removed stack %s: %s", stackName, strings.TrimSpace(string(output)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) StackExists(stackName string) (bool, error) {
|
||||
cmd := exec.Command("docker", "stack", "ls", "--format", "{{.Name}}")
|
||||
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("docker stack ls: %w", err)
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
if strings.TrimSpace(line) == stackName {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>a250.ca - Activate</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--border: #2a2d3a;
|
||||
--text: #e4e4e7;
|
||||
--muted: #a1a1aa;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--green: #22c55e;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.container { max-width: 520px; width: 100%; }
|
||||
.logo { font-size: 2rem; font-weight: 800; letter-spacing: -0.04em; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1.1rem; font-weight: 600; margin-bottom: 2rem; }
|
||||
.subtitle-green { color: var(--green); }
|
||||
.subtitle-muted { color: var(--muted); }
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.card h2 { font-size: 1.15rem; font-weight: 600; margin-bottom: 0.75rem; }
|
||||
.card p { color: var(--muted); font-size: 0.95rem; line-height: 1.6; margin-bottom: 1.5rem; }
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 2rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn:hover { background: var(--accent-hover); }
|
||||
.icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">a250.ca</div>
|
||||
{{if .NeedLogin}}
|
||||
<div class="subtitle subtitle-muted">Almost there</div>
|
||||
<div class="card">
|
||||
<span class="icon">🔒</span>
|
||||
<h2>Sign In First</h2>
|
||||
<p>You need to sign in with your new password before activating your stack. If you haven't set your password yet, do that first.</p>
|
||||
<a href="{{.AutheliaURL}}" class="btn">Sign In</a>
|
||||
</div>
|
||||
{{else if .Ready}}
|
||||
<div class="subtitle subtitle-green">Welcome, {{.User}}</div>
|
||||
<div class="card">
|
||||
<span class="icon">⚡</span>
|
||||
<h2>Activate Your Stack</h2>
|
||||
<p>Your account is verified. Click below to provision your dedicated environment and get full access.</p>
|
||||
<form method="POST" action="/activate">
|
||||
<button type="submit" class="btn">Activate Now</button>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>a250.ca - Dashboard</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--border: #2a2d3a;
|
||||
--text: #e4e4e7;
|
||||
--muted: #a1a1aa;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--green: #22c55e;
|
||||
--red: #ef4444;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
.header {
|
||||
max-width: 720px;
|
||||
margin: 0 auto 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.header .logo { font-size: 1.5rem; font-weight: 800; letter-spacing: -0.04em; }
|
||||
.header .user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.container { max-width: 720px; margin: 0 auto; }
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; }
|
||||
.status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
.status-label { color: var(--muted); font-size: 0.9rem; }
|
||||
.status-value { font-weight: 600; font-size: 0.9rem; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.7rem;
|
||||
border-radius: 100px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-active { background: rgba(34,197,94,0.15); color: var(--green); }
|
||||
.badge-inactive { background: rgba(239,68,68,0.15); color: var(--red); }
|
||||
.stack-link {
|
||||
display: block;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-top: 0.75rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.stack-link:hover { border-color: var(--accent); }
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn:hover { background: var(--accent-hover); }
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.btn-outline:hover { border-color: var(--accent); color: var(--accent); background: transparent; }
|
||||
.actions { display: flex; gap: 0.75rem; margin-top: 1rem; flex-wrap: wrap; }
|
||||
.empty-state { text-align: center; padding: 3rem 1rem; color: var(--muted); }
|
||||
.empty-state p { margin-bottom: 1.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="logo">a250.ca</div>
|
||||
<div class="user-info">
|
||||
{{if .User}}{{.User}}{{else}}Not logged in{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{{if .IsSubscribed}}
|
||||
<div class="card">
|
||||
<h2>Subscription</h2>
|
||||
<div class="status-row">
|
||||
<span class="status-label">Status</span>
|
||||
<span class="badge badge-active">Active</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">Email</span>
|
||||
<span class="status-value">{{.Email}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Your Stack</h2>
|
||||
<p style="color: var(--muted); font-size: 0.9rem;">Your dedicated environment is live and accessible at:</p>
|
||||
<a class="stack-link" href="http://{{.User}}.{{.Domain}}">{{.User}}.{{.Domain}}</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Manage</h2>
|
||||
<div class="actions">
|
||||
<form method="POST" action="/portal" style="margin:0">
|
||||
<input type="hidden" name="customer_id" value="">
|
||||
<button type="submit" class="btn">Manage Subscription</button>
|
||||
</form>
|
||||
<a href="{{.AutheliaURL}}" class="btn btn-outline">Account Settings</a>
|
||||
</div>
|
||||
<p style="color: var(--muted); font-size: 0.8rem; margin-top: 1rem;">
|
||||
No refunds for the current billing period. Access continues until the end of your paid month.
|
||||
</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="card">
|
||||
<div class="empty-state">
|
||||
{{if .User}}
|
||||
<h2>No Active Subscription</h2>
|
||||
<p>You're signed in as <strong>{{.User}}</strong>, but you don't have an active subscription.</p>
|
||||
<a href="/" class="btn">Subscribe Now</a>
|
||||
{{else}}
|
||||
<h2>Sign In Required</h2>
|
||||
<p>Sign in to access your dashboard.</p>
|
||||
<a href="{{.AutheliaURL}}" class="btn">Sign In</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>a250.ca - Subscribe</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--border: #2a2d3a;
|
||||
--text: #e4e4e7;
|
||||
--muted: #a1a1aa;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.container { max-width: 480px; width: 100%; }
|
||||
.logo {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.tagline {
|
||||
color: var(--muted);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 2.5rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
}
|
||||
.card h2 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
.price { font-size: 2rem; font-weight: 700; margin-bottom: 1.5rem; }
|
||||
.price span { font-size: 0.9rem; font-weight: 400; color: var(--muted); }
|
||||
.features { list-style: none; margin-bottom: 2rem; }
|
||||
.features li { padding: 0.4rem 0; color: var(--muted); font-size: 0.95rem; }
|
||||
.features li::before { content: "\2713"; color: var(--accent); font-weight: 700; margin-right: 0.75rem; }
|
||||
form { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
input[type="email"] {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
input[type="email"]:focus { border-color: var(--accent); }
|
||||
input[type="email"]::placeholder { color: var(--muted); }
|
||||
button {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover { background: var(--accent-hover); }
|
||||
.footer { margin-top: 2rem; color: var(--muted); font-size: 0.8rem; text-align: center; }
|
||||
.footer a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">a250.ca</div>
|
||||
<p class="tagline">Your own managed infrastructure stack, provisioned instantly when you subscribe.</p>
|
||||
<div class="card">
|
||||
<h2>Monthly Plan</h2>
|
||||
<div class="price">$20.00 <span>/ month</span></div>
|
||||
<ul class="features">
|
||||
<li>Dedicated Docker stack</li>
|
||||
<li>Secure single sign-on</li>
|
||||
<li>Automatic provisioning</li>
|
||||
<li>Manage subscription anytime</li>
|
||||
</ul>
|
||||
<form method="POST" action="/subscribe">
|
||||
<input type="email" name="email" placeholder="you@example.com" required>
|
||||
<button type="submit">Subscribe Now</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Already subscribed? <a href="/dashboard">Go to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>a250.ca - Welcome</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #1a1d27;
|
||||
--border: #2a2d3a;
|
||||
--text: #e4e4e7;
|
||||
--muted: #a1a1aa;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--green: #22c55e;
|
||||
--yellow: #eab308;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.container { max-width: 560px; width: 100%; }
|
||||
.logo { font-size: 2rem; font-weight: 800; letter-spacing: -0.04em; margin-bottom: 0.5rem; }
|
||||
.subtitle { color: var(--green); font-size: 1.1rem; font-weight: 600; margin-bottom: 2rem; }
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.card h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; }
|
||||
.cred-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.6rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.cred-row:last-child { border-bottom: none; }
|
||||
.cred-label { color: var(--muted); font-size: 0.9rem; }
|
||||
.cred-value {
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
background: var(--bg);
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
user-select: all;
|
||||
}
|
||||
.warning {
|
||||
background: rgba(234, 179, 8, 0.1);
|
||||
border: 1px solid rgba(234, 179, 8, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--yellow);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.warning strong { color: var(--yellow); }
|
||||
.steps { list-style: none; counter-reset: step; }
|
||||
.steps li {
|
||||
counter-increment: step;
|
||||
padding: 0.6rem 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.steps li::before {
|
||||
content: counter(step);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.7rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn:hover { background: var(--accent-hover); }
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.btn-outline:hover { border-color: var(--accent); color: var(--accent); background: transparent; }
|
||||
.actions { margin-top: 1.5rem; display: flex; flex-wrap: wrap; gap: 0.75rem; }
|
||||
.returning { text-align: center; padding: 2rem; color: var(--muted); }
|
||||
.returning p { margin-bottom: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">a250.ca</div>
|
||||
{{if .IsNew}}
|
||||
<div class="subtitle">Payment successful — your account is ready!</div>
|
||||
|
||||
<div class="warning">
|
||||
<strong>Temporary password.</strong> Use this to sign in for the first time, then you will be asked to reset it to something you choose.
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Your Temporary Login</h2>
|
||||
<div class="cred-row">
|
||||
<span class="cred-label">Username</span>
|
||||
<span class="cred-value">{{.Username}}</span>
|
||||
</div>
|
||||
<div class="cred-row">
|
||||
<span class="cred-label">Temporary Password</span>
|
||||
<span class="cred-value">{{.Password}}</span>
|
||||
</div>
|
||||
<div class="cred-row">
|
||||
<span class="cred-label">Email</span>
|
||||
<span class="cred-value">{{.Email}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Getting Started</h2>
|
||||
<ol class="steps">
|
||||
<li>Copy your username and temporary password above</li>
|
||||
<li>Click "Sign In" — you'll be taken to the login page</li>
|
||||
<li>Log in with your temporary credentials</li>
|
||||
<li>You'll be prompted to set a new password of your choice</li>
|
||||
<li>Once signed in, visit the activation page to launch your stack</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a href="{{.ResetURL}}" class="btn">Sign In & Set Password</a>
|
||||
<a href="{{.ActivateURL}}" class="btn btn-outline">Activate Stack</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="subtitle">Welcome back!</div>
|
||||
<div class="card">
|
||||
<div class="returning">
|
||||
<p>Your account <strong>{{.Username}}</strong> is already set up.</p>
|
||||
<a href="{{.LoginURL}}" class="btn">Sign In</a>
|
||||
<a href="{{.DashboardURL}}" class="btn btn-outline">Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: traefik/whoami:latest
|
||||
environment:
|
||||
WHOAMI_NAME: "{{.Subdomain}}"
|
||||
networks:
|
||||
- traefik_net
|
||||
deploy:
|
||||
replicas: 1
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: "{{.TraefikNetwork}}"
|
||||
traefik.http.routers.customer-{{.ID}}-web.rule: "Host(`{{.Subdomain}}.{{.Domain}}`)"
|
||||
traefik.http.routers.customer-{{.ID}}-web.entrypoints: "web"
|
||||
traefik.http.routers.customer-{{.ID}}-web.middlewares: "authelia-auth@docker"
|
||||
traefik.http.services.customer-{{.ID}}-web.loadbalancer.server.port: "80"
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- backend
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
networks:
|
||||
traefik_net:
|
||||
external: true
|
||||
name: "{{.TraefikNetwork}}"
|
||||
backend:
|
||||
driver: overlay
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
driver: local
|
||||
|
|
@ -123,7 +123,7 @@ ssh macmini7 'docker service logs authelia_authelia | grep -i "failed\|missing"'
|
|||
### Test OAuth Integration
|
||||
```bash
|
||||
# Test OAuth endpoint accessibility
|
||||
curl -s https://login.nixc.us/.well-known/openid_configuration | jq .
|
||||
curl -s https://login.a250.ca/.well-known/openid_configuration | jq .
|
||||
|
||||
# Verify client configurations
|
||||
ssh macmini7 'docker service logs authelia_authelia | grep -i "oidc\|oauth"'
|
||||
|
|
|
|||
|
|
@ -61,11 +61,11 @@ Configure in Portainer → Settings → Authentication:
|
|||
- **OAuth Provider**: Custom
|
||||
- **Client ID**: `portainer`
|
||||
- **Client Secret**: `<from CI vault>`
|
||||
- **Authorization URL**: `https://login.nixc.us/api/oidc/authorization`
|
||||
- **Token URL**: `https://login.nixc.us/api/oidc/token`
|
||||
- **User Info URL**: `https://login.nixc.us/api/oidc/userinfo`
|
||||
- **Authorization URL**: `https://login.a250.ca/api/oidc/authorization`
|
||||
- **Token URL**: `https://login.a250.ca/api/oidc/token`
|
||||
- **User Info URL**: `https://login.a250.ca/api/oidc/userinfo`
|
||||
- **Scopes**: `openid email profile groups`
|
||||
- **Redirect URL**: `https://portainer.nixc.us/`
|
||||
- **Redirect URL**: `https://portainer.a250.ca/`
|
||||
|
||||
#### 3. Remove Traefik Middleware (Optional)
|
||||
Once OAuth is working, remove middleware protection:
|
||||
|
|
@ -105,8 +105,8 @@ Configure in Gitea → Site Administration → Authentication Sources:
|
|||
- **OAuth2 Provider**: OpenID Connect
|
||||
- **Client ID**: `gitea`
|
||||
- **Client Secret**: `<from CI vault>`
|
||||
- **OpenID Connect Auto Discovery URL**: `https://login.nixc.us/.well-known/openid_configuration`
|
||||
- **Icon URL**: `https://login.nixc.us/static/media/logo.png` (optional)
|
||||
- **OpenID Connect Auto Discovery URL**: `https://login.a250.ca/.well-known/openid_configuration`
|
||||
- **Icon URL**: `https://login.a250.ca/static/media/logo.png` (optional)
|
||||
|
||||
## 🔄 Deployment Process
|
||||
|
||||
|
|
@ -129,9 +129,9 @@ Set up OAuth in each service's admin interface using the URLs and client IDs abo
|
|||
## 🔍 Testing OAuth Flow
|
||||
|
||||
### Test Authentication Flow
|
||||
1. **Visit protected service** (e.g., `https://portainer.nixc.us`)
|
||||
1. **Visit protected service** (e.g., `https://portainer.a250.ca`)
|
||||
2. **Click OAuth login** button
|
||||
3. **Redirect to Authelia** (`https://login.nixc.us`)
|
||||
3. **Redirect to Authelia** (`https://login.a250.ca`)
|
||||
4. **Authenticate** with your credentials
|
||||
5. **Redirect back** to service with authentication
|
||||
6. **Access granted** with user information
|
||||
|
|
@ -139,7 +139,7 @@ Set up OAuth in each service's admin interface using the URLs and client IDs abo
|
|||
### Troubleshooting
|
||||
- **Check redirect URIs** match exactly (including trailing slashes)
|
||||
- **Verify client secrets** in CI vault match generated values
|
||||
- **Confirm Authelia** is accessible at `https://login.nixc.us`
|
||||
- **Confirm Authelia** is accessible at `https://login.a250.ca`
|
||||
- **Check service logs** for OAuth-specific error messages
|
||||
|
||||
## 🛡️ Security Considerations
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ docker compose -f docker-compose.dev.yml up -d
|
|||
```
|
||||
|
||||
### Important URLs
|
||||
- **Authelia**: https://login.nixc.us
|
||||
- **Authelia**: https://login.a250.ca
|
||||
- **Development**: http://localhost:9091
|
||||
- **Health Check**: https://login.nixc.us/api/health
|
||||
- **OIDC Discovery**: https://login.nixc.us/.well-known/openid_configuration
|
||||
- **Health Check**: https://login.a250.ca/api/health
|
||||
- **OIDC Discovery**: https://login.a250.ca/.well-known/openid_configuration
|
||||
|
||||
### Required Secrets (12 Total)
|
||||
- **Core Secrets (5)**: LDAP, JWT, encryption, session, SMTP
|
||||
|
|
@ -70,7 +70,7 @@ ssh macmini7 'docker service logs authelia_authelia --follow'
|
|||
ssh macmini7 'docker service logs authelia_authelia | grep -i secret'
|
||||
|
||||
# Test OAuth endpoints
|
||||
curl -s https://login.nixc.us/.well-known/openid_configuration | jq .
|
||||
curl -s https://login.a250.ca/.well-known/openid_configuration | jq .
|
||||
```
|
||||
|
||||
## 📞 Support
|
||||
|
|
|
|||
|
|
@ -180,9 +180,9 @@ force_pull_latest_images() {
|
|||
log "🚀 Force pulling latest images to ensure fresh deployment"
|
||||
|
||||
# Get the image names from docker-compose production file
|
||||
local authelia_image="git.nixc.us/nixius/authelia:production-authelia"
|
||||
local mariadb_image="git.nixc.us/nixius/authelia:production-mariadb"
|
||||
local redis_image="git.nixc.us/nixius/authelia:production-redis"
|
||||
local authelia_image="git.nixc.us/a250/authelia:production-authelia"
|
||||
local mariadb_image="git.nixc.us/a250/authelia:production-mariadb"
|
||||
local redis_image="git.nixc.us/a250/authelia:production-redis"
|
||||
|
||||
# Pull each image and capture new hashes
|
||||
log "Pulling Authelia image..."
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
x-authelia-env: &authelia-env
|
||||
X_AUTHELIA_EMAIL: authelia@nixc.us
|
||||
X_AUTHELIA_EMAIL: authelia@a250.ca
|
||||
X_AUTHELIA_SITE_NAME: ATLAS
|
||||
X_AUTHELIA_CONFIG_FILTERS: template
|
||||
X_AUTHELIA_LDAP_DOMAIN: dc=nixc,dc=us
|
||||
TRAEFIK_DOMAIN: nixc.us
|
||||
X_AUTHELIA_LDAP_DOMAIN: dc=a250,dc=ca
|
||||
TRAEFIK_DOMAIN: a250.ca
|
||||
|
||||
secrets:
|
||||
AUTHENTICATION_BACKEND_LDAP_PASSWORD:
|
||||
|
|
@ -54,7 +54,7 @@ volumes:
|
|||
|
||||
services:
|
||||
authelia:
|
||||
image: git.nixc.us/nixius/authelia:production-authelia
|
||||
image: git.nixc.us/a250/authelia:production-authelia
|
||||
command:
|
||||
- authelia
|
||||
- --config=/config/configuration.server.yml
|
||||
|
|
@ -93,22 +93,22 @@ services:
|
|||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == ingress.nixc.us
|
||||
- node.hostname == ingress.a250.ca
|
||||
labels:
|
||||
us.nixc.autodeploy: "true"
|
||||
us.a250.autodeploy: "true"
|
||||
homepage.group: Infrastructure
|
||||
homepage.name: Authelia
|
||||
homepage.href: https://login.nixc.us
|
||||
homepage.href: https://login.a250.ca
|
||||
homepage.description: ATLAS
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: traefik
|
||||
traefik.http.routers.authelia_authelia.rule: Host(`login.nixc.us`)
|
||||
traefik.http.routers.authelia_authelia.rule: Host(`login.a250.ca`)
|
||||
traefik.http.routers.authelia_authelia.entrypoints: websecure
|
||||
traefik.http.routers.authelia_authelia.tls: "true"
|
||||
traefik.http.routers.authelia_authelia.tls.certresolver: letsencryptresolver
|
||||
traefik.http.routers.authelia_authelia.service: authelia_authelia
|
||||
traefik.http.services.authelia_authelia.loadbalancer.server.port: 9091
|
||||
traefik.http.middlewares.authelia_authelia.forwardauth.address: http://authelia_authelia:9091/api/verify?rd=https://login.nixc.us/
|
||||
traefik.http.middlewares.authelia_authelia.forwardauth.address: http://authelia_authelia:9091/api/verify?rd=https://login.a250.ca/
|
||||
traefik.http.middlewares.authelia_authelia.forwardauth.trustForwardHeader: "true"
|
||||
traefik.http.middlewares.authelia_authelia.forwardauth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email
|
||||
traefik.http.middlewares.authelia-basic.forwardauth.address: http://authelia_authelia:9091/api/verify?auth=basic
|
||||
|
|
@ -127,7 +127,7 @@ services:
|
|||
max-file: "3"
|
||||
|
||||
redis:
|
||||
image: git.nixc.us/nixius/authelia:production-redis
|
||||
image: git.nixc.us/a250/authelia:production-redis
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- authelia_redis_data:/data:rw
|
||||
|
|
@ -143,9 +143,9 @@ services:
|
|||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == ingress.nixc.us
|
||||
- node.hostname == ingress.a250.ca
|
||||
labels:
|
||||
us.nixc.autodeploy: "true"
|
||||
us.a250.autodeploy: "true"
|
||||
traefik.enable: "false"
|
||||
# healthcheck:
|
||||
# test: ["CMD", "redis-cli", "ping"]
|
||||
|
|
@ -160,7 +160,7 @@ services:
|
|||
max-file: "3"
|
||||
|
||||
mariadb:
|
||||
image: git.nixc.us/nixius/authelia:production-mariadb
|
||||
image: git.nixc.us/a250/authelia:production-mariadb
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: authelia
|
||||
MYSQL_DATABASE: authelia
|
||||
|
|
@ -180,9 +180,9 @@ services:
|
|||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == ingress.nixc.us
|
||||
- node.hostname == ingress.a250.ca
|
||||
labels:
|
||||
us.nixc.autodeploy: "true"
|
||||
us.a250.autodeploy: "true"
|
||||
traefik.enable: "false"
|
||||
# healthcheck:
|
||||
# test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "authelia", "-pauthelia"]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
x-authelia-env: &authelia-env
|
||||
X_AUTHELIA_EMAIL: authelia@nixc.us
|
||||
X_AUTHELIA_EMAIL: authelia@a250.ca
|
||||
X_AUTHELIA_SITE_NAME: ATLAS
|
||||
X_AUTHELIA_CONFIG_FILTERS: template
|
||||
X_AUTHELIA_LDAP_DOMAIN: dc=nixc,dc=us
|
||||
TRAEFIK_DOMAIN: nixc.us
|
||||
X_AUTHELIA_LDAP_DOMAIN: dc=a250,dc=ca
|
||||
TRAEFIK_DOMAIN: a250.ca
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
|
@ -23,7 +23,7 @@ volumes:
|
|||
|
||||
services:
|
||||
authelia:
|
||||
image: git.nixc.us/nixius/authelia:staging-authelia
|
||||
image: git.nixc.us/a250/authelia:staging-authelia
|
||||
command:
|
||||
- authelia
|
||||
- --config=/config/configuration.server.yml
|
||||
|
|
@ -48,18 +48,18 @@ services:
|
|||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == ingress.nixc.us
|
||||
- node.hostname == ingress.a250.ca
|
||||
labels:
|
||||
us.nixc.autodeploy: "true"
|
||||
us.a250.autodeploy: "true"
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: traefik
|
||||
traefik.http.routers.staging-authelia_authelia.rule: Host(`staging.login.nixc.us`)
|
||||
traefik.http.routers.staging-authelia_authelia.rule: Host(`staging.login.a250.ca`)
|
||||
traefik.http.routers.staging-authelia_authelia.entrypoints: websecure
|
||||
traefik.http.routers.staging-authelia_authelia.tls: "true"
|
||||
traefik.http.routers.staging-authelia_authelia.tls.certresolver: letsencryptresolver
|
||||
traefik.http.routers.staging-authelia_authelia.service: authelia_authelia
|
||||
traefik.http.services.staging-authelia_authelia.loadbalancer.server.port: 9091
|
||||
traefik.http.middlewares.staging-authelia_authelia.forwardauth.address: http://authelia_authelia:9091/api/verify?rd=https://login.nixc.us/
|
||||
traefik.http.middlewares.staging-authelia_authelia.forwardauth.address: http://authelia_authelia:9091/api/verify?rd=https://login.a250.ca/
|
||||
traefik.http.middlewares.staging-authelia_authelia.forwardauth.trustForwardHeader: "true"
|
||||
traefik.http.middlewares.staging-authelia_authelia.forwardauth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email
|
||||
traefik.http.middlewares.staging-authelia-basic.forwardauth.address: http://authelia_authelia:9091/api/verify?auth=basic
|
||||
|
|
@ -72,7 +72,7 @@ services:
|
|||
max-file: "3"
|
||||
|
||||
redis:
|
||||
image: git.nixc.us/nixius/authelia:staging-redis
|
||||
image: git.nixc.us/a250/authelia:staging-redis
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- authelia_staging_redis_data:/data:rw
|
||||
|
|
@ -88,9 +88,9 @@ services:
|
|||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == ingress.nixc.us
|
||||
- node.hostname == ingress.a250.ca
|
||||
labels:
|
||||
us.nixc.autodeploy: "true"
|
||||
us.a250.autodeploy: "true"
|
||||
traefik.enable: "false"
|
||||
logging:
|
||||
driver: json-file
|
||||
|
|
@ -99,7 +99,7 @@ services:
|
|||
max-file: "3"
|
||||
|
||||
mariadb:
|
||||
image: git.nixc.us/nixius/authelia:staging-mariadb
|
||||
image: git.nixc.us/a250/authelia:staging-mariadb
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: authelia
|
||||
MYSQL_DATABASE: authelia
|
||||
|
|
@ -119,9 +119,9 @@ services:
|
|||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == ingress.nixc.us
|
||||
- node.hostname == ingress.a250.ca
|
||||
labels:
|
||||
us.nixc.autodeploy: "true"
|
||||
us.a250.autodeploy: "true"
|
||||
traefik.enable: "false"
|
||||
logging:
|
||||
driver: json-file
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ echo " 3. Go to 'Users' section"
|
|||
echo " 4. Click 'Create User'"
|
||||
echo " 5. Fill in details:"
|
||||
echo " - Username: testuser"
|
||||
echo " - Email: testuser@nixc.us"
|
||||
echo " - Email: testuser@a250.ca"
|
||||
echo " - Display Name: Test User"
|
||||
echo " - Password: password123"
|
||||
echo " 6. Click 'Create'"
|
||||
|
|
@ -134,7 +134,7 @@ echo "======="
|
|||
echo "• If login fails, check LLDAP user exists and password is correct"
|
||||
echo "• Check Authelia ACL rules in docker/authelia/config/configuration.acl.yml"
|
||||
echo "• Use 'docker-compose -f docker-compose.dev.yml logs authelia' for debugging"
|
||||
echo "• LLDAP users need to be in the correct Base DN: dc=nixc,dc=us"
|
||||
echo "• LLDAP users need to be in the correct Base DN: dc=a250,dc=ca"
|
||||
echo ""
|
||||
|
||||
echo -e "${GREEN}✅ Authentication testing environment ready!${NC}"
|
||||
|
|
|
|||
Loading…
Reference in New Issue