2023-09-04 17:32:45 +02:00
|
|
|
package app
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"net/http"
|
2023-09-06 02:23:06 +02:00
|
|
|
"strings"
|
2023-09-04 17:32:45 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.sleepycat.moe/sam/mercury/internal/database"
|
2023-09-06 02:23:06 +02:00
|
|
|
"git.sleepycat.moe/sam/mercury/web/api"
|
|
|
|
"github.com/go-chi/render"
|
2023-09-04 17:32:45 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-09-06 02:23:06 +02:00
|
|
|
func (app *App) APIAuth(scope database.TokenScope, anonAccess bool) func(next http.Handler) http.Handler {
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
header := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
|
|
|
if header == "" {
|
|
|
|
cookie, err := r.Cookie(database.TokenCookieName)
|
|
|
|
if err != nil || cookie.Value == "" {
|
|
|
|
if anonAccess { // no token supplied, but the endpoint allows anonymous access
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
render.Status(r, api.ErrCodeStatus(api.ErrInvalidToken))
|
|
|
|
render.JSON(w, r, api.Error{
|
|
|
|
Code: api.ErrInvalidToken,
|
|
|
|
Message: api.ErrCodeMessage(api.ErrInvalidToken),
|
2023-09-15 16:33:08 +02:00
|
|
|
Details: "No token supplied",
|
2023-09-06 02:23:06 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2023-09-15 16:33:08 +02:00
|
|
|
header = cookie.Value
|
2023-09-06 02:23:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
token, err := app.ParseToken(r.Context(), header)
|
|
|
|
if err != nil {
|
|
|
|
render.Status(r, api.ErrCodeStatus(api.ErrInvalidToken))
|
|
|
|
render.JSON(w, r, api.Error{
|
|
|
|
Code: api.ErrInvalidToken,
|
|
|
|
Message: api.ErrCodeMessage(api.ErrInvalidToken),
|
2023-09-15 16:33:08 +02:00
|
|
|
Details: "Could not parse token",
|
2023-09-06 02:23:06 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if token.Expires.Before(time.Now()) {
|
|
|
|
render.Status(r, api.ErrCodeStatus(api.ErrInvalidToken))
|
|
|
|
render.JSON(w, r, api.Error{
|
|
|
|
Code: api.ErrInvalidToken,
|
|
|
|
Message: api.ErrCodeMessage(api.ErrInvalidToken),
|
2023-09-15 16:33:08 +02:00
|
|
|
Details: "Token is expired",
|
2023-09-06 02:23:06 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !token.Scopes.Has(scope) {
|
|
|
|
render.Status(r, api.ErrCodeStatus(api.ErrMissingScope))
|
|
|
|
render.JSON(w, r, api.Error{
|
|
|
|
Code: api.ErrMissingScope,
|
|
|
|
Message: api.ErrCodeMessage(api.ErrMissingScope),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := context.WithValue(r.Context(), ctxKeyClaims, token)
|
|
|
|
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-04 17:32:45 +02:00
|
|
|
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
|
|
|
|
}
|