diff --git a/internal/server/api/accounts/router.go b/internal/server/api/accounts/router.go
index 65e6a12..e73cb21 100644
--- a/internal/server/api/accounts/router.go
+++ b/internal/server/api/accounts/router.go
@@ -2,10 +2,10 @@ package accounts
import (
"errors"
- "fmt"
"strconv"
"github.com/gin-gonic/gin"
+ "github.com/google/uuid"
"ruben/inventory2/internal/consts"
"ruben/inventory2/internal/domains/accounts"
@@ -31,6 +31,8 @@ func Routes(
accts: accts,
}
+ r.POST("", response.Handler(as.createAccount))
+
syncGroups := r.Group("/:acctID/inventory/sync-groups")
syncGroups.POST("", response.Handler(as.saveNewSyncGroup))
@@ -41,19 +43,17 @@ func Routes(
draftListings.DELETE("/:orderIndex", response.Handler(as.deleteSyncGroupListingDraft))
}
-// TODO: test this endpoint again, to make it work.
// POST /
func (s *accountSubrouter) createAccount(c *gin.Context) (response.Response, error) {
r := c.Request
ctx := r.Context()
- email := r.FormValue("email")
- if email == "" {
- return nil, response.BadRequest().Msg("no email provided")
- }
+
+ // TODO: get email from the user's account profile (how?) - will need to flesh out the account creation process later on
+ email := uuid.NewString() + "@example.com"
userID := auth.GetIdentity(ctx).User.UserID
- acct, err := s.accts.CreateAccount(ctx, userID, email)
+ _, err := s.accts.CreateAccount(ctx, userID, email)
if err != nil {
if errors.Is(err, consts.ErrConflict) {
return nil, response.Conflict().
@@ -62,7 +62,10 @@ func (s *accountSubrouter) createAccount(c *gin.Context) (response.Response, err
return nil, response.Errorf("failed to create account: %w", err)
}
- return response.SeeOther(fmt.Sprintf("/ui/accounts/%d", acct.AccountID)), nil
+ return response.StatusCreated().
+ // force a reload to ensure an sse connection is made.
+ // user should automatically be redirected to the appropriate page based on their current location and/or login/account statuses.
+ HXRefresh("true"), nil
}
// POST /:acctID/inventory/sync-groups/draft/listings
diff --git a/internal/server/response/body.go b/internal/server/response/body.go
index e1febff..1938601 100644
--- a/internal/server/response/body.go
+++ b/internal/server/response/body.go
@@ -59,6 +59,54 @@ func (b bodyRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(b)
}
+func (b bodyRes) HXPushURL(v string) Response {
+ return HXPushURL(v).wrap(b)
+}
+
+func (b bodyRes) HXLocation(v string) Response {
+ return HXLocation(v).wrap(b)
+}
+
+func (b bodyRes) HXPushUrl(v string) Response {
+ return HXPushUrl(v).wrap(b)
+}
+
+func (b bodyRes) HXRedirect(v string) Response {
+ return HXRedirect(v).wrap(b)
+}
+
+func (b bodyRes) HXRefresh(v string) Response {
+ return HXRefresh(v).wrap(b)
+}
+
+func (b bodyRes) HXReplaceUrl(v string) Response {
+ return HXReplaceUrl(v).wrap(b)
+}
+
+func (b bodyRes) HXReswap(v string) Response {
+ return HXReswap(v).wrap(b)
+}
+
+func (b bodyRes) HXRetarget(v string) Response {
+ return HXRetarget(v).wrap(b)
+}
+
+func (b bodyRes) HXReselect(v string) Response {
+ return HXReselect(v).wrap(b)
+}
+
+func (b bodyRes) HXTrigger(v string) Response {
+ return HXTrigger(v).wrap(b)
+}
+
+func (b bodyRes) HXTriggerAfterSettle(v string) Response {
+ return HXTriggerAfterSettle(v).wrap(b)
+}
+
+func (b bodyRes) HXTriggerAfterSwap(v string) Response {
+ return HXTriggerAfterSwap(v).wrap(b)
+}
+
func (b bodyRes) GetStatus() (int, bool) {
if b.res == nil {
return 0, false
@@ -67,6 +115,13 @@ func (b bodyRes) GetStatus() (int, bool) {
return b.res.GetStatus()
}
+func (b bodyRes) getHeaders() [][2]string {
+ if b.res != nil {
+ return b.res.getHeaders()
+ }
+ return nil
+}
+
func (b bodyRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if b.res == nil {
return 0, "", false
diff --git a/internal/server/response/cookie.go b/internal/server/response/cookie.go
index 1e6ee13..ec5b955 100644
--- a/internal/server/response/cookie.go
+++ b/internal/server/response/cookie.go
@@ -58,6 +58,54 @@ func (c cookieRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(c)
}
+func (c cookieRes) HXPushURL(v string) Response {
+ return HXPushURL(v).wrap(c)
+}
+
+func (c cookieRes) HXLocation(v string) Response {
+ return HXLocation(v).wrap(c)
+}
+
+func (c cookieRes) HXPushUrl(v string) Response {
+ return HXPushUrl(v).wrap(c)
+}
+
+func (c cookieRes) HXRedirect(v string) Response {
+ return HXRedirect(v).wrap(c)
+}
+
+func (c cookieRes) HXRefresh(v string) Response {
+ return HXRefresh(v).wrap(c)
+}
+
+func (c cookieRes) HXReplaceUrl(v string) Response {
+ return HXReplaceUrl(v).wrap(c)
+}
+
+func (c cookieRes) HXReswap(v string) Response {
+ return HXReswap(v).wrap(c)
+}
+
+func (c cookieRes) HXRetarget(v string) Response {
+ return HXRetarget(v).wrap(c)
+}
+
+func (c cookieRes) HXReselect(v string) Response {
+ return HXReselect(v).wrap(c)
+}
+
+func (c cookieRes) HXTrigger(v string) Response {
+ return HXTrigger(v).wrap(c)
+}
+
+func (c cookieRes) HXTriggerAfterSettle(v string) Response {
+ return HXTriggerAfterSettle(v).wrap(c)
+}
+
+func (c cookieRes) HXTriggerAfterSwap(v string) Response {
+ return HXTriggerAfterSwap(v).wrap(c)
+}
+
func (c cookieRes) GetStatus() (int, bool) {
if c.res == nil {
return 0, false
@@ -66,6 +114,13 @@ func (c cookieRes) GetStatus() (int, bool) {
return c.res.GetStatus()
}
+func (c cookieRes) getHeaders() [][2]string {
+ if c.res != nil {
+ return c.res.getHeaders()
+ }
+ return nil
+}
+
func (c cookieRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if c.res == nil {
return 0, "", false
diff --git a/internal/server/response/header.go b/internal/server/response/header.go
new file mode 100644
index 0000000..5c5fa61
--- /dev/null
+++ b/internal/server/response/header.go
@@ -0,0 +1,212 @@
+package response
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "ruben/inventory2/internal/server/redirect"
+)
+
+type (
+ headerRes struct {
+ name string
+ value string
+ res Response
+ }
+)
+
+var _ Response = headerRes{}
+
+func header(name, value string) Response {
+ return headerRes{
+ name: name,
+ value: value,
+ }
+}
+
+func HXPushURL(v string) Response {
+ return header("HX-Push-Url", v)
+}
+
+func HXLocation(v string) Response {
+ return header("HX-Location", v)
+}
+
+func HXPushUrl(v string) Response {
+ return header("HX-Push-Url", v)
+}
+
+func HXRedirect(v string) Response {
+ return header("HX-Redirect", v)
+}
+
+func HXRefresh(v string) Response {
+ return header("HX-Refresh", v)
+}
+
+func HXReplaceUrl(v string) Response {
+ return header("HX-Replace-Url", v)
+}
+
+func HXReswap(v string) Response {
+ return header("HX-Reswap", v)
+}
+
+func HXRetarget(v string) Response {
+ return header("HX-Retarget", v)
+}
+
+func HXReselect(v string) Response {
+ return header("HX-Reselect", v)
+}
+
+func HXTrigger(v string) Response {
+ return header("HX-Trigger", v)
+}
+
+func HXTriggerAfterSettle(v string) Response {
+ return header("HX-Trigger-After-Settle", v)
+}
+
+func HXTriggerAfterSwap(v string) Response {
+ return header("HX-Trigger-After-Swap", v)
+}
+
+func (h headerRes) String() string {
+ if h.res != nil {
+ return fmt.Sprintf(`{"header": {"name": %q, "value": %q}, "nested": %s}`, h.name, h.value, h.res)
+ }
+ return fmt.Sprintf(`{"header": {"name": %q, "value": %q}}`, h.name, h.value)
+}
+
+func (h headerRes) wrap(res Response) Response {
+ h.res = res
+ return h
+}
+
+func (h headerRes) Status(code int) Response {
+ return Status(code).wrap(h)
+}
+
+func (h headerRes) Header(name, value string) Response {
+ h.name = name
+ h.value = value
+ return h
+}
+
+func (h headerRes) Redirect(code redirect.Code, to string) Response {
+ return Redirect(code, to).wrap(h)
+}
+
+func (h headerRes) HTML(body []byte) Response {
+ return HTML(body).wrap(h)
+}
+
+func (h headerRes) JSON(body any) Response {
+ return JSON(body).wrap(h)
+}
+
+func (h headerRes) Body(body io.ReadCloser) Response {
+ return Body(body).wrap(h)
+}
+
+func (h headerRes) Cookie(ck http.Cookie) Response {
+ return Cookie(ck).wrap(h)
+}
+
+func (h headerRes) HXPushURL(v string) Response {
+ return HXPushURL(v).wrap(h)
+}
+
+func (h headerRes) HXLocation(v string) Response {
+ return HXLocation(v).wrap(h)
+}
+
+func (h headerRes) HXPushUrl(v string) Response {
+ return HXPushUrl(v).wrap(h)
+}
+
+func (h headerRes) HXRedirect(v string) Response {
+ return HXRedirect(v).wrap(h)
+}
+
+func (h headerRes) HXRefresh(v string) Response {
+ return HXRefresh(v).wrap(h)
+}
+
+func (h headerRes) HXReplaceUrl(v string) Response {
+ return HXReplaceUrl(v).wrap(h)
+}
+
+func (h headerRes) HXReswap(v string) Response {
+ return HXReswap(v).wrap(h)
+}
+
+func (h headerRes) HXRetarget(v string) Response {
+ return HXRetarget(v).wrap(h)
+}
+
+func (h headerRes) HXReselect(v string) Response {
+ return HXReselect(v).wrap(h)
+}
+
+func (h headerRes) HXTrigger(v string) Response {
+ return HXTrigger(v).wrap(h)
+}
+
+func (h headerRes) HXTriggerAfterSettle(v string) Response {
+ return HXTriggerAfterSettle(v).wrap(h)
+}
+
+func (h headerRes) HXTriggerAfterSwap(v string) Response {
+ return HXTriggerAfterSwap(v).wrap(h)
+}
+
+func (h headerRes) GetStatus() (int, bool) {
+ if h.res == nil {
+ return 0, false
+ }
+
+ return h.res.GetStatus()
+}
+
+func (h headerRes) getHeaders() [][2]string {
+ hdr := [2]string{h.name, h.value}
+ if h.res == nil {
+ return [][2]string{hdr}
+ }
+
+ return append(h.res.getHeaders(), hdr)
+}
+
+func (h headerRes) GetRedirect() (code redirect.Code, to string, ok bool) {
+ if h.res == nil {
+ return 0, "", false
+ }
+
+ return h.res.GetRedirect()
+}
+
+func (h headerRes) getBody() (body io.ReadCloser, ok bool, err error) {
+ if h.res == nil {
+ return nil, false, nil
+ }
+
+ return h.res.getBody()
+}
+
+func (h headerRes) getCookies() []http.Cookie {
+ if h.res != nil {
+ return h.res.getCookies()
+ }
+
+ return nil
+}
+
+func (h headerRes) getContentType() (contentType string, ok bool) {
+ if h.res != nil {
+ return h.res.getContentType()
+ }
+
+ return "", false
+}
diff --git a/internal/server/response/html.go b/internal/server/response/html.go
index e5f4951..c68f77c 100644
--- a/internal/server/response/html.go
+++ b/internal/server/response/html.go
@@ -60,6 +60,54 @@ func (h htmlRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(h)
}
+func (h htmlRes) HXPushURL(v string) Response {
+ return HXPushURL(v).wrap(h)
+}
+
+func (h htmlRes) HXLocation(v string) Response {
+ return HXLocation(v).wrap(h)
+}
+
+func (h htmlRes) HXPushUrl(v string) Response {
+ return HXPushUrl(v).wrap(h)
+}
+
+func (h htmlRes) HXRedirect(v string) Response {
+ return HXRedirect(v).wrap(h)
+}
+
+func (h htmlRes) HXRefresh(v string) Response {
+ return HXRefresh(v).wrap(h)
+}
+
+func (h htmlRes) HXReplaceUrl(v string) Response {
+ return HXReplaceUrl(v).wrap(h)
+}
+
+func (h htmlRes) HXReswap(v string) Response {
+ return HXReswap(v).wrap(h)
+}
+
+func (h htmlRes) HXRetarget(v string) Response {
+ return HXRetarget(v).wrap(h)
+}
+
+func (h htmlRes) HXReselect(v string) Response {
+ return HXReselect(v).wrap(h)
+}
+
+func (h htmlRes) HXTrigger(v string) Response {
+ return HXTrigger(v).wrap(h)
+}
+
+func (h htmlRes) HXTriggerAfterSettle(v string) Response {
+ return HXTriggerAfterSettle(v).wrap(h)
+}
+
+func (h htmlRes) HXTriggerAfterSwap(v string) Response {
+ return HXTriggerAfterSwap(v).wrap(h)
+}
+
func (h htmlRes) GetStatus() (int, bool) {
if h.res == nil {
return 0, false
@@ -68,6 +116,13 @@ func (h htmlRes) GetStatus() (int, bool) {
return h.res.GetStatus()
}
+func (h htmlRes) getHeaders() [][2]string {
+ if h.res != nil {
+ return h.res.getHeaders()
+ }
+ return nil
+}
+
func (h htmlRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if h.res == nil {
return 0, "", false
diff --git a/internal/server/response/json.go b/internal/server/response/json.go
index 0e823cc..70696a3 100644
--- a/internal/server/response/json.go
+++ b/internal/server/response/json.go
@@ -61,6 +61,54 @@ func (j jsonRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(j)
}
+func (j jsonRes) HXPushURL(v string) Response {
+ return HXPushURL(v).wrap(j)
+}
+
+func (j jsonRes) HXLocation(v string) Response {
+ return HXLocation(v).wrap(j)
+}
+
+func (j jsonRes) HXPushUrl(v string) Response {
+ return HXPushUrl(v).wrap(j)
+}
+
+func (j jsonRes) HXRedirect(v string) Response {
+ return HXRedirect(v).wrap(j)
+}
+
+func (j jsonRes) HXRefresh(v string) Response {
+ return HXRefresh(v).wrap(j)
+}
+
+func (j jsonRes) HXReplaceUrl(v string) Response {
+ return HXReplaceUrl(v).wrap(j)
+}
+
+func (j jsonRes) HXReswap(v string) Response {
+ return HXReswap(v).wrap(j)
+}
+
+func (j jsonRes) HXRetarget(v string) Response {
+ return HXRetarget(v).wrap(j)
+}
+
+func (j jsonRes) HXReselect(v string) Response {
+ return HXReselect(v).wrap(j)
+}
+
+func (j jsonRes) HXTrigger(v string) Response {
+ return HXTrigger(v).wrap(j)
+}
+
+func (j jsonRes) HXTriggerAfterSettle(v string) Response {
+ return HXTriggerAfterSettle(v).wrap(j)
+}
+
+func (j jsonRes) HXTriggerAfterSwap(v string) Response {
+ return HXTriggerAfterSwap(v).wrap(j)
+}
+
func (j jsonRes) GetStatus() (int, bool) {
if j.res == nil {
return 0, false
@@ -69,6 +117,13 @@ func (j jsonRes) GetStatus() (int, bool) {
return j.res.GetStatus()
}
+func (j jsonRes) getHeaders() [][2]string {
+ if j.res != nil {
+ return j.res.getHeaders()
+ }
+ return nil
+}
+
func (j jsonRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if j.res == nil {
return 0, "", false
diff --git a/internal/server/response/redirect.go b/internal/server/response/redirect.go
index 3479db6..405dfb8 100644
--- a/internal/server/response/redirect.go
+++ b/internal/server/response/redirect.go
@@ -99,6 +99,54 @@ func (r redirectRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(r)
}
+func (r redirectRes) HXPushURL(v string) Response {
+ return HXPushURL(v).wrap(r)
+}
+
+func (r redirectRes) HXLocation(v string) Response {
+ return HXLocation(v).wrap(r)
+}
+
+func (r redirectRes) HXPushUrl(v string) Response {
+ return HXPushUrl(v).wrap(r)
+}
+
+func (r redirectRes) HXRedirect(v string) Response {
+ return HXRedirect(v).wrap(r)
+}
+
+func (r redirectRes) HXRefresh(v string) Response {
+ return HXRefresh(v).wrap(r)
+}
+
+func (r redirectRes) HXReplaceUrl(v string) Response {
+ return HXReplaceUrl(v).wrap(r)
+}
+
+func (r redirectRes) HXReswap(v string) Response {
+ return HXReswap(v).wrap(r)
+}
+
+func (r redirectRes) HXRetarget(v string) Response {
+ return HXRetarget(v).wrap(r)
+}
+
+func (r redirectRes) HXReselect(v string) Response {
+ return HXReselect(v).wrap(r)
+}
+
+func (r redirectRes) HXTrigger(v string) Response {
+ return HXTrigger(v).wrap(r)
+}
+
+func (r redirectRes) HXTriggerAfterSettle(v string) Response {
+ return HXTriggerAfterSettle(v).wrap(r)
+}
+
+func (r redirectRes) HXTriggerAfterSwap(v string) Response {
+ return HXTriggerAfterSwap(v).wrap(r)
+}
+
func (r redirectRes) GetStatus() (int, bool) {
if r.res == nil {
return 0, false
@@ -107,6 +155,13 @@ func (r redirectRes) GetStatus() (int, bool) {
return r.res.GetStatus()
}
+func (r redirectRes) getHeaders() [][2]string {
+ if r.res != nil {
+ return r.res.getHeaders()
+ }
+ return nil
+}
+
func (r redirectRes) GetRedirect() (code redirect.Code, to string, ok bool) {
return r.code, r.to, true
}
diff --git a/internal/server/response/response.go b/internal/server/response/response.go
index 7b5de48..dbc90b5 100644
--- a/internal/server/response/response.go
+++ b/internal/server/response/response.go
@@ -10,6 +10,18 @@ import (
type (
Response interface {
Status(int) Response
+ HXPushURL(string) Response
+ HXLocation(string) Response
+ HXPushUrl(string) Response
+ HXRedirect(string) Response
+ HXRefresh(string) Response
+ HXReplaceUrl(string) Response
+ HXReswap(string) Response
+ HXRetarget(string) Response
+ HXReselect(string) Response
+ HXTrigger(string) Response
+ HXTriggerAfterSettle(string) Response
+ HXTriggerAfterSwap(string) Response
Redirect(code redirect.Code, to string) Response
Body(io.ReadCloser) Response
HTML([]byte) Response
@@ -21,6 +33,7 @@ type (
GetRedirect() (code redirect.Code, to string, ok bool)
getCookies() []http.Cookie
getContentType() (contentType string, ok bool)
+ getHeaders() [][2]string
wrap(Response) Response
}
diff --git a/internal/server/response/status.go b/internal/server/response/status.go
index 078989f..4ac1b58 100644
--- a/internal/server/response/status.go
+++ b/internal/server/response/status.go
@@ -76,10 +76,65 @@ func (s statusRes) Cookie(ck http.Cookie) Response {
return Cookie(ck).wrap(s)
}
+func (s statusRes) HXPushURL(v string) Response {
+ return HXPushURL(v).wrap(s)
+}
+
+func (s statusRes) HXLocation(v string) Response {
+ return HXLocation(v).wrap(s)
+}
+
+func (s statusRes) HXPushUrl(v string) Response {
+ return HXPushUrl(v).wrap(s)
+}
+
+func (s statusRes) HXRedirect(v string) Response {
+ return HXRedirect(v).wrap(s)
+}
+
+func (s statusRes) HXRefresh(v string) Response {
+ return HXRefresh(v).wrap(s)
+}
+
+func (s statusRes) HXReplaceUrl(v string) Response {
+ return HXReplaceUrl(v).wrap(s)
+}
+
+func (s statusRes) HXReswap(v string) Response {
+ return HXReswap(v).wrap(s)
+}
+
+func (s statusRes) HXRetarget(v string) Response {
+ return HXRetarget(v).wrap(s)
+}
+
+func (s statusRes) HXReselect(v string) Response {
+ return HXReselect(v).wrap(s)
+}
+
+func (s statusRes) HXTrigger(v string) Response {
+ return HXTrigger(v).wrap(s)
+}
+
+func (s statusRes) HXTriggerAfterSettle(v string) Response {
+ return HXTriggerAfterSettle(v).wrap(s)
+}
+
+func (s statusRes) HXTriggerAfterSwap(v string) Response {
+ return HXTriggerAfterSwap(v).wrap(s)
+}
+
func (s statusRes) GetStatus() (int, bool) {
return s.code, true
}
+func (s statusRes) getHeaders() [][2]string {
+ if s.res != nil {
+ return s.res.getHeaders()
+ }
+ return nil
+}
+
func (s statusRes) GetRedirect() (code redirect.Code, to string, ok bool) {
if s.res == nil {
return 0, "", false
diff --git a/internal/server/response/write.go b/internal/server/response/write.go
index 98224c8..a56a260 100644
--- a/internal/server/response/write.go
+++ b/internal/server/response/write.go
@@ -72,12 +72,16 @@ func writeResponse(c *gin.Context, res Response) {
// w.Header() must be set before ResponseWriter.WriteHeader is called
// or redirect is attempted
hdrs := w.Header()
+ for _, hdr := range res.getHeaders() {
+ hdrs.Add(hdr[0], hdr[1])
+ }
+
for _, ck := range res.getCookies() {
hdrs.Add("Set-Cookie", ck.String())
}
if ct, ok := res.getContentType(); ok {
- hdrs.Add("Content-Type", ct)
+ hdrs.Set("Content-Type", ct)
}
if code, to, ok := res.GetRedirect(); ok {
diff --git a/internal/server/ui/router.go b/internal/server/ui/router.go
index 2edfded..8df51d9 100644
--- a/internal/server/ui/router.go
+++ b/internal/server/ui/router.go
@@ -93,10 +93,19 @@ func Routes(
etsy: etsy,
}
- r.GET("", response.Handler(s.serveTemplate))
+ r.GET("", response.Handler(s.redirectToAccountsIfLoggedInWithAnAccount), response.Handler(s.serveTemplate))
r.GET("/*rest", authenticate, response.Handler(s.serveTemplate))
}
+func (s *webpageRouter) redirectToAccountsIfLoggedInWithAnAccount(c *gin.Context) (response.Response, error) {
+ id := auth.GetIdentity(c)
+ if id.Account == nil || id.Account.AccountID == 0 {
+ return nil, nil
+ }
+
+ return response.TemporaryRedirect(fmt.Sprintf("%s/accounts/%d", s.uiPath, id.Account.AccountID)), nil
+}
+
// GET /
// compiles the page template or component template matching the url
func (s *webpageRouter) serveTemplate(c *gin.Context) (response.Response, error) {
@@ -221,7 +230,33 @@ func authorizeByMatchingAccountID(r *http.Request, acctIDPathPosition int) error
id := auth.GetIdentity(r.Context())
if id.Account == nil || id.Account.AccountID != acctID {
return response.Unauthorized().
- Msgf("user does not have access to account %d", acctID)
+ HTML([]byte(fmt.Sprintf(`
+
+ Sorry, you don't have access to the given page.
+
+ Access Not Granted
+
+
+
+
+ /ui%s
+
+
+
Synchronize inventory automatically!
diff --git a/templates/pages/unauthorized.html.tmpl b/templates/pages/unauthorized.html.tmpl
new file mode 100644
index 0000000..2bab4a7
--- /dev/null
+++ b/templates/pages/unauthorized.html.tmpl
@@ -0,0 +1,20 @@
+
+ Sorry, you don't have access to the page you were looking for.
+
+ Access Not Granted
+
+
+ {{ $link := printf "/ui%s" .Request.URL }}
+
+
+ {{ $link }}
+
+
+