diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index f4fc07f..d6f8b60 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -1,47 +1,64 @@
-
- Vite + Svelte + Go
+
+ Vite + Svelte + Go
-
-
-
+
+
+
-
- Check out SvelteKit, the official Svelte app framework powered by Vite!
-
+
+ Check out SvelteKit, the official Svelte app framework powered by Vite!
+
-
- Click on the Vite and Svelte logos to learn more
-
+ {#if account}
+
+ Username: {account.username}, ID: {account.id}
+
+ {/if}
+
+ Click on the Vite and Svelte logos to learn more
diff --git a/frontend/src/lib/api/account.ts b/frontend/src/lib/api/account.ts
new file mode 100644
index 0000000..28adff2
--- /dev/null
+++ b/frontend/src/lib/api/account.ts
@@ -0,0 +1,9 @@
+export interface Account {
+ id: string;
+ username: string;
+ domain: string | null;
+}
+
+export interface MeAccount extends Account {
+ email: string;
+}
diff --git a/internal/database/sql/token.go b/internal/database/sql/token.go
index 9f224b2..8af1bac 100644
--- a/internal/database/sql/token.go
+++ b/internal/database/sql/token.go
@@ -34,7 +34,7 @@ func (s *TokenStore) GetApplication(ctx context.Context, id ulid.ULID) (database
return app, errors.Wrap(err, "executing query")
}
-func (s *TokenStore) Create(ctx context.Context, userID, appID ulid.ULID, scopes []string, expires time.Time) (database.Token, error) {
+func (s *TokenStore) Create(ctx context.Context, userID, appID ulid.ULID, scopes database.TokenScopes, expires time.Time) (database.Token, error) {
q := sqlf.Sprintf(`INSERT INTO tokens
(id, user_id, app_id, scopes, expires)
values (%s, %s, %s, %v, %v)
diff --git a/internal/database/token.go b/internal/database/token.go
index 0477d66..f40f582 100644
--- a/internal/database/token.go
+++ b/internal/database/token.go
@@ -19,7 +19,7 @@ type Token struct {
ID ulid.ULID
AppID ulid.ULID
UserID ulid.ULID
- Scopes []string
+ Scopes TokenScopes
Expires time.Time
}
@@ -43,3 +43,34 @@ type TokenClaims struct {
UserID ulid.ULID `json:"sub"`
jwt.RegisteredClaims
}
+
+type TokenScope string
+
+const (
+ // All scopes below
+ TokenScopeAll TokenScope = "all"
+ TokenScopeAccountsRead TokenScope = "accounts.read"
+ // Controls whether tokens have access to sensitive account data, NOT if they can use `/accounts/@me` endpoints.
+ TokenScopeAccountsMe TokenScope = "accounts.me"
+ TokenScopeAccountsWrite TokenScope = "accounts.write"
+)
+
+func (s TokenScope) IsValid() bool {
+ switch s {
+ case TokenScopeAccountsRead, TokenScopeAccountsMe, TokenScopeAccountsWrite:
+ return true
+ default:
+ return false
+ }
+}
+
+type TokenScopes []TokenScope
+
+func (s TokenScopes) Has(scope TokenScope) bool {
+ for i := range s {
+ if s[i] == scope || s[i] == TokenScopeAll {
+ return true
+ }
+ }
+ return false
+}
diff --git a/web/api/user.go b/web/api/user.go
new file mode 100644
index 0000000..ecd3822
--- /dev/null
+++ b/web/api/user.go
@@ -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,
+ }
+}
diff --git a/web/app/middleware.go b/web/app/middleware.go
new file mode 100644
index 0000000..f55cc96
--- /dev/null
+++ b/web/app/middleware.go
@@ -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
+}
diff --git a/web/app/template.go b/web/app/template.go
index f1e3023..a2bfc68 100644
--- a/web/app/template.go
+++ b/web/app/template.go
@@ -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 {
diff --git a/web/auth/login.go b/web/auth/login.go
index 194a633..523b5eb 100644
--- a/web/auth/login.go
+++ b/web/auth/login.go
@@ -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
diff --git a/web/frontend/app.html b/web/frontend/app.html
index 4110bf2..39cb513 100644
--- a/web/frontend/app.html
+++ b/web/frontend/app.html
@@ -9,5 +9,7 @@
+
+