forked from Nixius/authelia
Compare commits
7 Commits
c68edc70d1
...
bd84b0a578
| Author | SHA1 | Date |
|---|---|---|
|
|
bd84b0a578 | |
|
|
bbc828fa35 | |
|
|
1f8f50d50b | |
|
|
91c0411b90 | |
|
|
aa1201560d | |
|
|
c7d19ed20d | |
|
|
677bef195f |
|
|
@ -4,10 +4,27 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (a *App) handleResendReset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username := r.FormValue("username")
|
||||||
|
if username == "" {
|
||||||
|
http.Error(w, "username required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := a.triggerPasswordReset(username); err != nil {
|
||||||
|
log.Printf("resend-reset: failed for %s: %v", username, err)
|
||||||
|
http.Error(w, "failed to send email", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("resend-reset: password reset email sent for %s", username)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Password setup email sent. Check your inbox."))
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) triggerPasswordReset(username string) error {
|
func (a *App) triggerPasswordReset(username string) error {
|
||||||
body, _ := json.Marshal(map[string]string{"username": username})
|
body, _ := json.Marshal(map[string]string{"username": username})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,14 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
remoteGroups := r.Header.Get("Remote-Groups")
|
remoteGroups := r.Header.Get("Remote-Groups")
|
||||||
isSubscribed := remoteGroups != "" && contains(remoteGroups, "customers")
|
isSubscribed := remoteGroups != "" && contains(remoteGroups, "customers")
|
||||||
|
|
||||||
|
// Authelia session may be stale (user was added to customers after login).
|
||||||
|
if !isSubscribed && remoteUser != "" {
|
||||||
|
inGroup, _ := a.ldap.IsInGroup(remoteUser, "customers")
|
||||||
|
if inGroup {
|
||||||
|
isSubscribed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var customerID string
|
var customerID string
|
||||||
stackDeployed := false
|
stackDeployed := false
|
||||||
stackRunning := false
|
stackRunning := false
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ type App struct {
|
||||||
|
|
||||||
func NewRouter(cfg *config.Config, sc *ssstripe.Client, lc *ldap.Client, sw *swarm.Client) http.Handler {
|
func NewRouter(cfg *config.Config, sc *ssstripe.Client, lc *ldap.Client, sw *swarm.Client) http.Handler {
|
||||||
tmplPattern := filepath.Join(cfg.TemplatePath, "pages", "*.html")
|
tmplPattern := filepath.Join(cfg.TemplatePath, "pages", "*.html")
|
||||||
tmpl := template.Must(template.ParseGlob(tmplPattern))
|
partialsPattern := filepath.Join(cfg.TemplatePath, "partials", "*.html")
|
||||||
|
tmpl := template.Must(template.Must(template.ParseGlob(partialsPattern)).ParseGlob(tmplPattern))
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
|
@ -47,6 +48,7 @@ func NewRouter(cfg *config.Config, sc *ssstripe.Client, lc *ldap.Client, sw *swa
|
||||||
r.Get("/dashboard", app.handleDashboard)
|
r.Get("/dashboard", app.handleDashboard)
|
||||||
r.Post("/stack-manage", app.handleStackManage)
|
r.Post("/stack-manage", app.handleStackManage)
|
||||||
r.Post("/subscribe", app.handleCreateCheckout)
|
r.Post("/subscribe", app.handleCreateCheckout)
|
||||||
|
r.Post("/resend-reset", app.handleResendReset)
|
||||||
r.Post("/portal", app.handlePortal)
|
r.Post("/portal", app.handlePortal)
|
||||||
r.Post("/resubscribe", app.handleResubscribe)
|
r.Post("/resubscribe", app.handleResubscribe)
|
||||||
r.Post("/webhook/stripe", app.handleWebhook)
|
r.Post("/webhook/stripe", app.handleWebhook)
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,22 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) handleLanding(w http.ResponseWriter, r *http.Request) {
|
func (a *App) handleLanding(w http.ResponseWriter, r *http.Request) {
|
||||||
|
remoteUser := r.Header.Get("Remote-User")
|
||||||
|
|
||||||
if contains(r.Header.Get("Remote-Groups"), "customers") {
|
if contains(r.Header.Get("Remote-Groups"), "customers") {
|
||||||
http.Redirect(w, r, a.cfg.AppURL+"/dashboard", http.StatusSeeOther)
|
http.Redirect(w, r, a.cfg.AppURL+"/dashboard", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logged-in user who paid but hasn't activated yet — send to activate.
|
||||||
|
if remoteUser != "" {
|
||||||
|
custID, _ := a.ldap.GetStripeCustomerID(remoteUser)
|
||||||
|
if custID != "" {
|
||||||
|
http.Redirect(w, r, a.cfg.AppURL+"/activate", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"AppURL": a.cfg.AppURL,
|
"AppURL": a.cfg.AppURL,
|
||||||
"Commit": version.Commit,
|
"Commit": version.Commit,
|
||||||
|
|
@ -72,18 +84,19 @@ func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.IsNew {
|
inGroup, _ := a.ldap.IsInGroup(result.Username, "customers")
|
||||||
// New user: send password setup email, show onboarding page.
|
|
||||||
// Group membership and stack deploy happen on /activate after they set a password.
|
if result.IsNew || !inGroup {
|
||||||
|
// New or lapsed customer: send password setup email, show onboarding.
|
||||||
|
// Group membership and stack deploy happen on /activate after they log in.
|
||||||
if err := a.triggerPasswordReset(result.Username); err != nil {
|
if err := a.triggerPasswordReset(result.Username); err != nil {
|
||||||
log.Printf("authelia reset trigger failed for %s: %v", username, err)
|
log.Printf("authelia reset trigger failed for %s: %v", username, err)
|
||||||
}
|
}
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"Username": result.Username,
|
"Username": result.Username,
|
||||||
"IsNew": true,
|
"IsNew": result.IsNew,
|
||||||
"Email": email,
|
"Email": email,
|
||||||
"LoginURL": a.cfg.AutheliaURL,
|
"LoginURL": a.cfg.AutheliaURL,
|
||||||
"ResetURL": a.cfg.AutheliaURL + "/#/reset-password/step1",
|
|
||||||
"ActivateURL": a.cfg.AppURL + "/activate",
|
"ActivateURL": a.cfg.AppURL + "/activate",
|
||||||
"DashboardURL": a.cfg.AppURL + "/dashboard",
|
"DashboardURL": a.cfg.AppURL + "/dashboard",
|
||||||
"InstanceURL": "https://" + result.Username + "." + a.cfg.CustomerDomain,
|
"InstanceURL": "https://" + result.Username + "." + a.cfg.CustomerDomain,
|
||||||
|
|
@ -95,17 +108,7 @@ func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing user resubscribing: re-add to customers group if needed and
|
// Returning active customer: ensure stack exists, go to dashboard
|
||||||
// ensure their stack is running, then send straight to dashboard.
|
|
||||||
inGroup, _ := a.ldap.IsInGroup(result.Username, "customers")
|
|
||||||
if !inGroup {
|
|
||||||
if err := a.ldap.AddToGroup(result.Username, "customers"); err != nil {
|
|
||||||
log.Printf("resubscribe: add to group failed for %s: %v", result.Username, err)
|
|
||||||
} else {
|
|
||||||
log.Printf("resubscribe: re-added %s to customers group", result.Username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stackName := fmt.Sprintf("customer-%s", result.Username)
|
stackName := fmt.Sprintf("customer-%s", result.Username)
|
||||||
exists, _ := a.swarm.StackExists(stackName)
|
exists, _ := a.swarm.StackExists(stackName)
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|
@ -113,7 +116,6 @@ func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("resubscribe: stack deploy failed for %s: %v", result.Username, err)
|
log.Printf("resubscribe: stack deploy failed for %s: %v", result.Username, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("resubscribe: %s payment verified, redirecting to dashboard", result.Username)
|
log.Printf("resubscribe: %s payment verified, redirecting to dashboard", result.Username)
|
||||||
http.Redirect(w, r, a.cfg.AppURL+"/dashboard", http.StatusSeeOther)
|
http.Redirect(w, r, a.cfg.AppURL+"/dashboard", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ func (a *App) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconciliation: ensures LLDAP user exists. Group + stack are handled by /activate.
|
// Reconciliation backstop: ensures LLDAP user + Stripe ID are set.
|
||||||
|
// Does NOT send password reset — that's the success page's responsibility
|
||||||
|
// so it can reliably show the welcome/onboarding page.
|
||||||
func (a *App) onCheckoutCompleted(event stripego.Event) {
|
func (a *App) onCheckoutCompleted(event stripego.Event) {
|
||||||
var sess stripego.CheckoutSession
|
var sess stripego.CheckoutSession
|
||||||
if err := json.Unmarshal(event.Data.Raw, &sess); err != nil {
|
if err := json.Unmarshal(event.Data.Raw, &sess); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
{{template "analytics"}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@
|
||||||
user-select: all;
|
user-select: all;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
{{template "analytics"}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@
|
||||||
user-select: all;
|
user-select: all;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
{{template "analytics"}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
{{template "analytics"}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
@ -177,7 +178,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href="{{.ResetURL}}" class="btn">Resend / Set Password</a>
|
<button type="button" class="btn" id="reset-btn-new"
|
||||||
|
onclick="sendReset(this,'{{.Username}}')">Resend Set Password Email</button>
|
||||||
<a href="{{.ActivateURL}}" class="btn btn-outline">Activate Stack</a>
|
<a href="{{.ActivateURL}}" class="btn btn-outline">Activate Stack</a>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
@ -204,11 +206,34 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a href="{{.ResetURL}}" class="btn">Reset Password</a>
|
<button type="button" class="btn" id="reset-btn-returning"
|
||||||
|
onclick="sendReset(this,'{{.Username}}')">Resend Set Password Email</button>
|
||||||
<a href="{{.LoginURL}}" class="btn btn-outline">Sign In</a>
|
<a href="{{.LoginURL}}" class="btn btn-outline">Sign In</a>
|
||||||
<a href="{{.DashboardURL}}" class="btn btn-outline">Dashboard</a>
|
<a href="{{.DashboardURL}}" class="btn btn-outline">Dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
function sendReset(btn, username) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Sending…';
|
||||||
|
fetch('/resend-reset', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: 'username=' + encodeURIComponent(username)
|
||||||
|
}).then(function(r) {
|
||||||
|
if (r.ok) {
|
||||||
|
btn.textContent = 'Email sent — check your inbox';
|
||||||
|
btn.style.background = 'var(--green)';
|
||||||
|
} else {
|
||||||
|
btn.textContent = 'Failed — try again';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}).catch(function() {
|
||||||
|
btn.textContent = 'Failed — try again';
|
||||||
|
btn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{{define "analytics"}}
|
||||||
|
<!-- Matomo -->
|
||||||
|
<script>
|
||||||
|
var _paq = window._paq = window._paq || [];
|
||||||
|
_paq.push(['trackPageView']);
|
||||||
|
_paq.push(['enableLinkTracking']);
|
||||||
|
(function() {
|
||||||
|
var u="//metrics.nixc.us/";
|
||||||
|
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||||
|
_paq.push(['setSiteId', '12']);
|
||||||
|
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||||
|
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- End Matomo -->
|
||||||
|
<!-- PostHog -->
|
||||||
|
<script>
|
||||||
|
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group identify setPersonProperties setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroups onFeatureFlags addFeatureFlagsHandler onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||||
|
posthog.init('phc_VP3GGVVmRQx2j1PjOvmiznCLuW7YHp1Hvk218FXPsgR', {
|
||||||
|
api_host: 'https://eu.i.posthog.com',
|
||||||
|
defaults: '2026-01-30'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<!-- End PostHog -->
|
||||||
|
{{end}}
|
||||||
|
|
@ -77,7 +77,7 @@ services:
|
||||||
echo "$${CLIENT_SECRET_HEADADMIN}" > /run/secrets/CLIENT_SECRET_HEADADMIN
|
echo "$${CLIENT_SECRET_HEADADMIN}" > /run/secrets/CLIENT_SECRET_HEADADMIN
|
||||||
echo "$${CLIENT_SECRET_PORTAINER}" > /run/secrets/CLIENT_SECRET_PORTAINER
|
echo "$${CLIENT_SECRET_PORTAINER}" > /run/secrets/CLIENT_SECRET_PORTAINER
|
||||||
echo "$${CLIENT_SECRET_GITEA}" > /run/secrets/CLIENT_SECRET_GITEA
|
echo "$${CLIENT_SECRET_GITEA}" > /run/secrets/CLIENT_SECRET_GITEA
|
||||||
{ echo 'access_control:'; echo ' default_policy: deny'; echo ' rules:'; echo ' - domain: login.bc.a250.ca'; echo ' policy: bypass'; echo ' - domain: app.bc.a250.ca'; echo ' policy: bypass'; echo ' resources:'; echo " - '^/$$'"; echo " - '^/subscribe$$'"; echo " - '^/success(\\?.*)?$$'"; echo " - '^/webhook/stripe$$'"; echo " - '^/health$$'"; echo " - '^/version$$'"; echo ' - domain: app.bc.a250.ca'; echo ' policy: one_factor'; echo ' resources:'; echo " - '^/dashboard$$'"; echo " - '^/activate$$'"; echo " - '^/portal$$'"; echo " - '^/resubscribe$$'"; echo " - '^/stack-manage$$'"; echo ' - domain:'; echo ' - lldap.bc.a250.ca'; echo ' - whoami.bc.a250.ca'; echo ' policy: bypass'; echo ' - domain: "{user}.bc.a250.ca"'; echo ' policy: one_factor'; echo ' - domain: "*.bc.a250.ca"'; echo ' policy: deny'; } > /config/configuration.acl.yml
|
{ echo 'access_control:'; echo ' default_policy: deny'; echo ' rules:'; echo ' - domain: login.bc.a250.ca'; echo ' policy: bypass'; echo ' - domain: app.bc.a250.ca'; echo ' policy: bypass'; echo ' resources:'; echo " - '^/$$'"; echo " - '^/subscribe$$'"; echo " - '^/success(\\?.*)?$$'"; echo " - '^/webhook/stripe$$'"; echo " - '^/resend-reset$$'"; echo " - '^/health$$'"; echo " - '^/version$$'"; echo ' - domain: app.bc.a250.ca'; echo ' policy: one_factor'; echo ' resources:'; echo " - '^/dashboard$$'"; echo " - '^/activate$$'"; echo " - '^/portal$$'"; echo " - '^/resubscribe$$'"; echo " - '^/stack-manage$$'"; echo ' - domain:'; echo ' - lldap.bc.a250.ca'; echo ' - whoami.bc.a250.ca'; echo ' policy: bypass'; echo ' - domain: "{user}.bc.a250.ca"'; echo ' policy: one_factor'; echo ' - domain: "*.bc.a250.ca"'; echo ' policy: deny'; } > /config/configuration.acl.yml
|
||||||
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
|
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:
|
environment:
|
||||||
X_AUTHELIA_EMAIL: authelia@a250.ca
|
X_AUTHELIA_EMAIL: authelia@a250.ca
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue