diff --git a/backend/db/tokens.go b/backend/db/tokens.go
index b812dae..70c87cb 100644
--- a/backend/db/tokens.go
+++ b/backend/db/tokens.go
@@ -14,6 +14,8 @@ type Token struct {
UserID xid.ID
TokenID xid.ID
Invalidated bool
+ APIOnly bool `db:"api_only"`
+ ReadOnly bool
Created time.Time
Expires time.Time
}
@@ -62,10 +64,15 @@ func (db *DB) Tokens(ctx context.Context, userID xid.ID) (ts []Token, err error)
const ExpiryTime = 3 * 30 * 24 * time.Hour
// SaveToken saves a token to the database.
-func (db *DB) SaveToken(ctx context.Context, userID xid.ID, tokenID xid.ID) (t Token, err error) {
+func (db *DB) SaveToken(ctx context.Context, userID xid.ID, tokenID xid.ID, apiOnly, readOnly bool) (t Token, err error) {
sql, args, err := sq.Insert("tokens").
- Columns("user_id", "token_id", "expires").
- Values(userID, tokenID, time.Now().Add(ExpiryTime)).
+ SetMap(map[string]any{
+ "user_id": userID,
+ "token_id": tokenID,
+ "expires": time.Now().Add(ExpiryTime),
+ "api_only": apiOnly,
+ "read_only": readOnly,
+ }).
Suffix("RETURNING *").
ToSql()
if err != nil {
diff --git a/backend/routes/auth/discord.go b/backend/routes/auth/discord.go
index f0f37c7..369cc38 100644
--- a/backend/routes/auth/discord.go
+++ b/backend/routes/auth/discord.go
@@ -117,7 +117,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
}
// save token to database
- _, err = s.DB.SaveToken(ctx, u.ID, tokenID)
+ _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
if err != nil {
return errors.Wrap(err, "saving token to database")
}
@@ -343,7 +343,7 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
}
// save token to database
- _, err = s.DB.SaveToken(ctx, u.ID, tokenID)
+ _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
if err != nil {
return errors.Wrap(err, "saving token to database")
}
diff --git a/backend/routes/auth/fedi_mastodon.go b/backend/routes/auth/fedi_mastodon.go
index 0f40d4d..ef2a81d 100644
--- a/backend/routes/auth/fedi_mastodon.go
+++ b/backend/routes/auth/fedi_mastodon.go
@@ -138,7 +138,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
}
// save token to database
- _, err = s.DB.SaveToken(ctx, u.ID, tokenID)
+ _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
if err != nil {
return errors.Wrap(err, "saving token to database")
}
@@ -371,7 +371,7 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
}
// save token to database
- _, err = s.DB.SaveToken(ctx, u.ID, tokenID)
+ _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
if err != nil {
return errors.Wrap(err, "saving token to database")
}
diff --git a/backend/routes/auth/fedi_misskey.go b/backend/routes/auth/fedi_misskey.go
index 07fcc46..6331def 100644
--- a/backend/routes/auth/fedi_misskey.go
+++ b/backend/routes/auth/fedi_misskey.go
@@ -118,7 +118,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
}
// save token to database
- _, err = s.DB.SaveToken(ctx, u.ID, tokenID)
+ _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
if err != nil {
return errors.Wrap(err, "saving token to database")
}
@@ -301,7 +301,7 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
}
// save token to database
- _, err = s.DB.SaveToken(ctx, u.ID, tokenID)
+ _, err = s.DB.SaveToken(ctx, u.ID, tokenID, false, false)
if err != nil {
return errors.Wrap(err, "saving token to database")
}
diff --git a/backend/routes/auth/tokens.go b/backend/routes/auth/tokens.go
index 4705abc..5a79042 100644
--- a/backend/routes/auth/tokens.go
+++ b/backend/routes/auth/tokens.go
@@ -12,16 +12,20 @@ import (
)
type getTokenResponse struct {
- TokenID xid.ID `json:"id"`
- Created time.Time `json:"created"`
- Expires time.Time `json:"expires"`
+ TokenID xid.ID `json:"id"`
+ APIOnly bool `json:"api_only"`
+ ReadOnly bool `json:"read_only"`
+ Created time.Time `json:"created"`
+ Expires time.Time `json:"expires"`
}
func dbTokenToGetResponse(t db.Token) getTokenResponse {
return getTokenResponse{
- TokenID: t.TokenID,
- Created: t.Created,
- Expires: t.Expires,
+ TokenID: t.TokenID,
+ APIOnly: t.APIOnly,
+ ReadOnly: t.ReadOnly,
+ Created: t.Created,
+ Expires: t.Expires,
}
}
@@ -47,7 +51,7 @@ func (s *Server) deleteToken(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
claims, _ := server.ClaimsFromContext(ctx)
- if !claims.TokenWrite || claims.APIToken {
+ if claims.APIToken {
return server.APIError{Code: server.ErrInvalidToken}
}
@@ -71,7 +75,42 @@ func (s *Server) deleteToken(w http.ResponseWriter, r *http.Request) error {
return nil
}
-func (s *Server) createToken(w http.ResponseWriter, r *http.Request) error {
- // unimplemented right now
- return server.APIError{Code: server.ErrForbidden}
+type createTokenResponse struct {
+ Token string `json:"token"`
+ TokenID xid.ID `json:"id"`
+ APIOnly bool `json:"api_only"`
+ ReadOnly bool `json:"read_only"`
+ Created time.Time `json:"created"`
+ Expires time.Time `json:"expires"`
+}
+
+func (s *Server) createToken(w http.ResponseWriter, r *http.Request) error {
+ ctx := r.Context()
+ claims, _ := server.ClaimsFromContext(ctx)
+
+ if claims.APIToken {
+ return server.APIError{Code: server.ErrInvalidToken}
+ }
+
+ readOnly := r.FormValue("read_only") == "true"
+ tokenID := xid.New()
+ tokenStr, err := s.Auth.CreateToken(claims.UserID, tokenID, false, true, !readOnly)
+ if err != nil {
+ return errors.Wrap(err, "creating token")
+ }
+
+ t, err := s.DB.SaveToken(ctx, claims.UserID, tokenID, true, readOnly)
+ if err != nil {
+ return errors.Wrap(err, "saving token")
+ }
+
+ render.JSON(w, r, createTokenResponse{
+ Token: tokenStr,
+ TokenID: t.TokenID,
+ APIOnly: t.APIOnly,
+ ReadOnly: t.ReadOnly,
+ Created: t.Created,
+ Expires: t.Expires,
+ })
+ return nil
}
diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte
index 5905d61..a112ef4 100644
--- a/frontend/src/routes/settings/+page.svelte
+++ b/frontend/src/routes/settings/+page.svelte
@@ -17,11 +17,6 @@
$: usernameValid = usernameRegex.test(username);
let error: APIError | null = null;
- let deleteOpen = false;
- const toggleDeleteOpen = () => (deleteOpen = !deleteOpen);
- let deleteUsername = "";
- let deleteError: APIError | null = null;
-
const changeUsername = async () => {
try {
const resp = await apiFetchClientYour profile
@@ -93,6 +116,31 @@
+ If you think one of your tokens might have been compromised, you can log out on all devices + by clicking this button. +
++ +
+If you want to force log out on all devices, click the button below.
+ {#if invalidateError} +ID | -Created at | -Expires at | -Current? | - - - {#each data.tokens as token} -
---|---|---|---|
{token.id} |
- {DateTime.fromISO(token.created).toLocal().toLocaleString(DateTime.DATETIME_MED)} | -{DateTime.fromISO(token.expires).toLocal().toLocaleString(DateTime.DATETIME_MED)} | -{#if claims["jti"] === token.id} |
-
Created a new API token! Please save it somewhere secure, as it will only be shown once.
+{newToken}