feat(auth): support API tokens and bearer token authentication

- Add backend services to create, list, and delete API tokens.
- Implement Bearer token authentication to resolve tokens to users.
- Register HTTP routes for managing user tokens under `/account/tokens`.
- Add tests to verify that uploads with valid Bearer tokens associate the upload with the correct user, while invalid tokens fall back to anonymous uploads.
This commit is contained in:
2026-05-31 12:50:13 +03:00
parent 0503fad9af
commit d99f8ee82a
9 changed files with 533 additions and 3 deletions

View File

@@ -67,6 +67,61 @@ func TestLoggedInUploadStoresOwnerAndAnonymousUploadDoesNot(t *testing.T) {
}
}
func TestBearerTokenUploadActsAsUser(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()
user, err := app.authService.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
if err != nil {
t.Fatalf("CreateBootstrapUser returned error: %v", err)
}
tokenResult, err := app.authService.CreateAPIToken(user.ID, "cli")
if err != nil {
t.Fatalf("CreateAPIToken returned error: %v", err)
}
request := multipartUploadRequest(t, "/api/v1/upload", "file", "owned.txt", "owned")
request.Header.Set("Accept", "application/json")
request.Header.Set("Authorization", "Bearer "+tokenResult.Plaintext)
response := httptest.NewRecorder()
app.Upload(response, request)
if response.Code != http.StatusCreated {
t.Fatalf("token upload status = %d, body = %s", response.Code, response.Body.String())
}
var payload services.UploadResult
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
t.Fatalf("json.Unmarshal returned error: %v", err)
}
box, err := app.uploadService.GetBox(payload.BoxID)
if err != nil {
t.Fatalf("GetBox returned error: %v", err)
}
if box.OwnerID != user.ID {
t.Fatalf("OwnerID = %q, want %q", box.OwnerID, user.ID)
}
// An invalid bearer token must not authenticate as the user.
badRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "x.txt", "x")
badRequest.Header.Set("Accept", "application/json")
badRequest.Header.Set("Authorization", "Bearer wbx_bogus.secret")
badResponse := httptest.NewRecorder()
app.Upload(badResponse, badRequest)
if badResponse.Code != http.StatusCreated {
t.Fatalf("anonymous fallback upload status = %d, body = %s", badResponse.Code, badResponse.Body.String())
}
var badPayload services.UploadResult
if err := json.Unmarshal(badResponse.Body.Bytes(), &badPayload); err != nil {
t.Fatalf("json.Unmarshal returned error: %v", err)
}
badBox, err := app.uploadService.GetBox(badPayload.BoxID)
if err != nil {
t.Fatalf("GetBox returned error: %v", err)
}
if badBox.OwnerID != "" {
t.Fatalf("invalid token OwnerID = %q, want empty", badBox.OwnerID)
}
}
func TestInviteHandlerCreatesUserAndMarksInviteUsed(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()