add frontend auth middleware, embed user data in frontend html

This commit is contained in:
sam 2023-09-04 17:32:45 +02:00
parent d8cb8c8fa8
commit 0fa769a248
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
12 changed files with 265 additions and 42 deletions

33
web/api/user.go Normal file
View file

@ -0,0 +1,33 @@
package api
import (
"git.sleepycat.moe/sam/mercury/internal/database"
"github.com/oklog/ulid/v2"
)
// Account is the basic user returned by endpoints, without any private data.
type Account struct {
ID ulid.ULID `json:"id"`
Username string `json:"username"`
Domain *string `json:"domain"`
}
type SelfAccount struct {
Account
Email string `json:"email"`
}
func DBAccountToAccount(a database.Account) Account {
return Account{
ID: a.ID,
Username: a.Username,
Domain: a.Domain,
}
}
func DBAccountToSelfAccount(a database.Account) SelfAccount {
return SelfAccount{
Account: DBAccountToAccount(a),
Email: *a.Email,
}
}

53
web/app/middleware.go Normal file
View file

@ -0,0 +1,53 @@
package app
import (
"context"
"net/http"
"time"
"git.sleepycat.moe/sam/mercury/internal/database"
)
type ctxKey int
const (
ctxKeyClaims ctxKey = 1
)
func (app *App) FrontendAuth(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie(database.TokenCookieName)
if err != nil || cookie.Value == "" {
next.ServeHTTP(w, r)
return
}
token, err := app.ParseToken(r.Context(), cookie.Value)
if err != nil {
app.ErrorTemplate(w, r, "Invalid token", "The provided token was not valid. Try clearing your cookies and reloading the page.")
return
}
if token.Expires.Before(time.Now()) {
http.SetCookie(w, &http.Cookie{Name: database.TokenCookieName, Value: "", Expires: time.Now()})
next.ServeHTTP(w, r)
return
}
ctx := context.WithValue(r.Context(), ctxKeyClaims, token)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
func (app *App) TokenFromContext(ctx context.Context) (database.Token, bool) {
v := ctx.Value(ctxKeyClaims)
if v == nil {
return database.Token{}, false
}
claims, ok := v.(database.Token)
return claims, ok
}

View file

@ -7,6 +7,13 @@ import (
"github.com/flosch/pongo2/v6"
)
func (app *App) ErrorTemplate(w http.ResponseWriter, r *http.Request, header, desc string) error {
return app.Template(w, r, "error.tpl", pongo2.Context{
"header": header,
"description": desc,
})
}
func (app *App) Template(w http.ResponseWriter, r *http.Request, tmplName string, ctx pongo2.Context) error {
tmpl, err := app.tmpl.FromCache(tmplName)
if err != nil {

View file

@ -70,7 +70,7 @@ func (app *Auth) PostLogin(w http.ResponseWriter, r *http.Request) {
// create a new token
token, err := app.Token(conn).Create(
ctx, acct.ID, *app.DBConfig.Get().InternalApplication, []string{"all"}, time.Now().Add(math.MaxInt64))
ctx, acct.ID, *app.DBConfig.Get().InternalApplication, database.TokenScopes{database.TokenScopeAll}, time.Now().Add(math.MaxInt64))
if err != nil {
log.Err(err).Msg("creating token")
return

View file

@ -9,5 +9,7 @@
</head>
<body>
<div id="app"></div>
<script type="text/json" id="accountData">{{.AccountData}}</script>
</body>
</html>

View file

@ -2,6 +2,7 @@ package frontend
import (
"embed"
"encoding/json"
"html/template"
"net/http"
"os"
@ -9,6 +10,7 @@ import (
"git.sleepycat.moe/sam/mercury/frontend"
"git.sleepycat.moe/sam/mercury/internal/database"
"git.sleepycat.moe/sam/mercury/web/api"
"git.sleepycat.moe/sam/mercury/web/app"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
@ -89,10 +91,65 @@ func (app *Frontend) ServeAssets(w http.ResponseWriter, r *http.Request) {
}
func (app *Frontend) ServeFrontend(w http.ResponseWriter, r *http.Request) {
_, ok := app.TokenFromContext(r.Context())
if !ok {
http.Redirect(w, r, "/auth/login", http.StatusSeeOther)
return
}
app.serveFrontend(w, r)
}
func (app *Frontend) ServeUser(w http.ResponseWriter, r *http.Request) {
_, ok := app.TokenFromContext(r.Context())
if !ok {
username := chi.URLParam(r, "username")
http.Redirect(w, r, "/@"+username, http.StatusSeeOther)
return
}
app.serveFrontend(w, r)
}
func (app *Frontend) ServeStatus(w http.ResponseWriter, r *http.Request) {
_, ok := app.TokenFromContext(r.Context())
if !ok {
username := chi.URLParam(r, "username")
postID := chi.URLParam(r, "postID")
http.Redirect(w, r, "/@"+username+"/posts/"+postID, http.StatusSeeOther)
return
}
app.serveFrontend(w, r)
}
func (app *Frontend) serveFrontend(w http.ResponseWriter, r *http.Request) {
token, ok := app.TokenFromContext(r.Context())
if !ok {
log.Error().Msg("app.serveFrontend was called without a token being set")
http.Redirect(w, r, "/auth/login", http.StatusSeeOther)
return
}
acct, err := app.Account().ByID(r.Context(), token.UserID)
if err != nil {
log.Err(err).Msg("fetching account")
app.ErrorTemplate(w, r, "Internal server error", "An internal server error occurred. Please try again later.")
return
}
b, err := json.Marshal(api.DBAccountToSelfAccount(acct))
if err != nil {
log.Err(err).Msg("marshaling account json")
app.ErrorTemplate(w, r, "Internal server error", "An internal server error occurred. Please try again later.")
return
}
w.Header().Set("Content-Type", "text/html")
err := app.tmpl.Execute(w, map[string]any{
"Config": database.DefaultConfig,
"Vue": app.glue,
err = app.tmpl.Execute(w, map[string]any{
"Config": database.DefaultConfig,
"Vue": app.glue,
"AccountData": template.HTML(b),
})
if err != nil {
log.Err(err).Msg("executing frontend template")

View file

@ -22,9 +22,13 @@ func Routes(app *app.App) {
// also assets
app.Router.Group(func(r chi.Router) {
frontend := frontend.New(app)
r.Use(app.FrontendAuth)
r.HandleFunc(frontend.AssetsPath(), frontend.ServeAssets)
r.HandleFunc("/static/*", frontend.ServeStaticAssets)
r.HandleFunc("/web", frontend.ServeFrontend)
r.HandleFunc("/web/*", frontend.ServeFrontend)
r.HandleFunc("/web/@{username}", frontend.ServeUser)
r.HandleFunc("/web/@{username}/posts/{postID}", frontend.ServeStatus)
})
}

10
web/templates/error.tpl Normal file
View file

@ -0,0 +1,10 @@
{% extends 'base.tpl' %}
{% block title %}
Error &bull; {{ config.Name }}
{% endblock %}
{% block content %}
<div class="error">
<h1>{{ heading }}</h1>
<p>{{ description }}</p>
</div>
{% endblock %}