Skip to content

Commit de718b8

Browse files
authored
Merge pull request #3 from database-playground/pan93412/dbp-15-implement-logoutuser-in-api
feat: implement logoutUser
2 parents 1b83352 + f1cd921 commit de718b8

File tree

11 files changed

+117
-280
lines changed

11 files changed

+117
-280
lines changed

cmd/backend/dependencies.go

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,6 @@ func AuthStorage(redisClient rueidis.Client) auth.Storage {
3535
return auth.NewRedisStorage(redisClient)
3636
}
3737

38-
// AuthMiddleware creates an auth.Middleware that can be injected into gin.
39-
func AuthMiddleware(storage auth.Storage) Middleware {
40-
return Middleware{
41-
Handler: auth.Middleware(storage),
42-
}
43-
}
44-
45-
// MachineMiddleware creates a machine middleware that can be injected into gin.
46-
func MachineMiddleware() Middleware {
47-
return Middleware{
48-
Handler: httputils.MachineMiddleware(),
49-
}
50-
}
51-
52-
// CorsMiddleware creates a cors middleware that can be injected into gin.
53-
func CorsMiddleware(cfg config.Config) Middleware {
54-
return Middleware{
55-
Handler: cors.New(cors.Config{
56-
AllowOrigins: cfg.AllowedOrigins,
57-
AllowMethods: []string{"GET", "POST", "OPTIONS"},
58-
AllowHeaders: []string{"Content-Type", "User-Agent", "Referer"},
59-
AllowCredentials: true,
60-
}),
61-
}
62-
}
63-
6438
func SqlRunner(cfg config.Config) *sqlrunner.SqlRunner {
6539
return sqlrunner.NewSqlRunner(cfg.SqlRunner)
6640
}
@@ -92,24 +66,29 @@ func AuthService(entClient *ent.Client, storage auth.Storage, config config.Conf
9266
}
9367

9468
// GinEngine creates a gin engine.
95-
func GinEngine(services []httpapi.Service, middlewares []Middleware, gqlgenHandler *handler.Server, cfg config.Config) *gin.Engine {
69+
func GinEngine(services []httpapi.Service, authStorage auth.Storage, gqlgenHandler *handler.Server, cfg config.Config) *gin.Engine {
9670
engine := gin.New()
9771

9872
if err := engine.SetTrustedProxies(cfg.TrustProxies); err != nil {
9973
slog.Error("error setting trusted proxies", "error", err)
10074
}
10175

102-
for _, middleware := range middlewares {
103-
engine.Use(middleware.Handler)
104-
}
105-
10676
engine.Use(gin.Recovery())
107-
108-
engine.GET("/", func(ctx *gin.Context) {
77+
engine.Use(httputils.MachineMiddleware())
78+
engine.Use(cors.New(cors.Config{
79+
AllowOrigins: cfg.AllowedOrigins,
80+
AllowMethods: []string{"GET", "POST", "OPTIONS"},
81+
AllowHeaders: []string{"Content-Type", "User-Agent", "Referer"},
82+
AllowCredentials: true,
83+
}))
84+
85+
router := engine.Group("/")
86+
router.Use(auth.Middleware(authStorage))
87+
router.GET("/", func(ctx *gin.Context) {
10988
handler := playground.Handler("GraphQL playground", "/query")
11089
handler.ServeHTTP(ctx.Writer, ctx.Request)
11190
})
112-
engine.POST("/query", func(ctx *gin.Context) {
91+
router.POST("/query", func(ctx *gin.Context) {
11392
gqlgenHandler.ServeHTTP(ctx.Writer, ctx.Request)
11493
})
11594

@@ -174,19 +153,6 @@ func GinLifecycle(lifecycle fx.Lifecycle, engine *gin.Engine, cfg config.Config)
174153
})
175154
}
176155

177-
// Middleware is a middleware that can be injected into gin.
178-
type Middleware struct {
179-
Handler gin.HandlerFunc
180-
}
181-
182-
// AnnotateMiddleware annotates a middleware function to be injected into gin.
183-
func AnnotateMiddleware(f any) any {
184-
return fx.Annotate(
185-
f,
186-
fx.ResultTags(`group:"middlewares"`),
187-
)
188-
}
189-
190156
// AnnotateService annotates a service function to be injected into gin.
191157
func AnnotateService(f any) any {
192158
return fx.Annotate(

cmd/backend/server.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@ func main() {
1616
fx.Provide(
1717
AuthStorage,
1818
SqlRunner,
19-
AnnotateMiddleware(AuthMiddleware),
20-
AnnotateMiddleware(MachineMiddleware),
21-
AnnotateMiddleware(CorsMiddleware),
2219
AnnotateService(AuthService),
2320
GqlgenHandler,
2421
fx.Annotate(
2522
GinEngine,
26-
fx.ParamTags(`group:"services"`, `group:"middlewares"`),
23+
fx.ParamTags(`group:"services"`),
2724
),
2825
),
2926
fx.Invoke(GinLifecycle),

graph/user.graphqls

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,14 @@ extend type Mutation {
9292
impersonateUser(userID: ID!): String! @scope(scope: "user:impersonate")
9393

9494
"""
95-
Logout from all the devices of the current user.
95+
Logout a user from all his devices.
9696
"""
97-
logoutAll: Boolean! @scope(scope: "me:write")
97+
logoutUser(userID: ID!): Boolean! @scope(scope: "user:write")
9898

9999
"""
100-
Delete the current user.
100+
Logout from all the devices of the current user.
101101
"""
102-
deleteMe: Boolean! @scope(scope: "me:delete")
102+
logoutAll: Boolean! @scope(scope: "me:write")
103103

104104
"""
105105
Verify the registration of this user.

graph/user.resolvers.go

Lines changed: 6 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

graph/user.resolvers_test.go

Lines changed: 0 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,165 +1251,6 @@ func TestMutationResolver_UpdateMe(t *testing.T) {
12511251
})
12521252
}
12531253

1254-
func TestMutationResolver_DeleteMe(t *testing.T) {
1255-
t.Run("success", func(t *testing.T) {
1256-
entClient := testhelper.NewEntSqliteClient(t)
1257-
1258-
// Setup database with proper groups and scopes
1259-
_, err := setup.Setup(context.Background(), entClient)
1260-
require.NoError(t, err)
1261-
1262-
// Get the new-user group (which has me:delete scope)
1263-
newUserGroup, err := entClient.Group.Query().Where(group.NameEQ(useraccount.NewUserGroupSlug)).Only(context.Background())
1264-
require.NoError(t, err)
1265-
1266-
// Create a test user in new-user group
1267-
user, err := entClient.User.Create().
1268-
SetName("testuser").
1269-
SetEmail("[email protected]").
1270-
SetGroup(newUserGroup).
1271-
Save(context.Background())
1272-
require.NoError(t, err)
1273-
1274-
resolver := &Resolver{
1275-
ent: entClient,
1276-
auth: &mockAuthStorage{},
1277-
}
1278-
1279-
// Create test server with scope directive
1280-
cfg := Config{
1281-
Resolvers: resolver,
1282-
Directives: DirectiveRoot{Scope: directive.ScopeDirective},
1283-
}
1284-
srv := handler.New(NewExecutableSchema(cfg))
1285-
srv.AddTransport(transport.POST{})
1286-
c := client.New(srv)
1287-
1288-
// Execute mutation
1289-
var resp struct {
1290-
DeleteMe bool
1291-
}
1292-
err = c.Post(`mutation { deleteMe }`, &resp, func(bd *client.Request) {
1293-
bd.HTTP = bd.HTTP.WithContext(auth.WithUser(bd.HTTP.Context(), auth.TokenInfo{
1294-
UserID: user.ID,
1295-
Scopes: []string{"me:delete"},
1296-
}))
1297-
})
1298-
1299-
// Verify response
1300-
require.NoError(t, err)
1301-
require.True(t, resp.DeleteMe)
1302-
1303-
// Verify user was actually deleted
1304-
_, err = entClient.User.Get(context.Background(), user.ID)
1305-
require.Error(t, err)
1306-
require.True(t, ent.IsNotFound(err))
1307-
})
1308-
1309-
t.Run("unauthenticated", func(t *testing.T) {
1310-
entClient := testhelper.NewEntSqliteClient(t)
1311-
resolver := &Resolver{
1312-
ent: entClient,
1313-
auth: &mockAuthStorage{},
1314-
}
1315-
1316-
// Create test server with scope directive
1317-
cfg := Config{
1318-
Resolvers: resolver,
1319-
Directives: DirectiveRoot{Scope: directive.ScopeDirective},
1320-
}
1321-
srv := handler.New(NewExecutableSchema(cfg))
1322-
srv.AddTransport(transport.POST{})
1323-
c := client.New(srv)
1324-
1325-
// Execute mutation with no auth context
1326-
var resp struct {
1327-
DeleteMe bool
1328-
}
1329-
err := c.Post(`mutation { deleteMe }`, &resp)
1330-
1331-
// Verify error
1332-
require.Error(t, err)
1333-
require.Contains(t, err.Error(), defs.ErrUnauthorized.Error())
1334-
})
1335-
1336-
t.Run("insufficient scope", func(t *testing.T) {
1337-
entClient := testhelper.NewEntSqliteClient(t)
1338-
1339-
resolver := &Resolver{
1340-
ent: entClient,
1341-
auth: &mockAuthStorage{},
1342-
}
1343-
1344-
// Create test server with scope directive
1345-
cfg := Config{
1346-
Resolvers: resolver,
1347-
Directives: DirectiveRoot{Scope: directive.ScopeDirective},
1348-
}
1349-
srv := handler.New(NewExecutableSchema(cfg))
1350-
srv.AddTransport(transport.POST{})
1351-
c := client.New(srv)
1352-
1353-
// Create context with authenticated user but wrong scope
1354-
ctx := auth.WithUser(context.Background(), auth.TokenInfo{
1355-
UserID: 1,
1356-
Scopes: []string{"user:read"},
1357-
})
1358-
1359-
// Execute mutation
1360-
var resp struct {
1361-
DeleteMe bool
1362-
}
1363-
err := c.Post(`mutation { deleteMe }`, &resp, func(bd *client.Request) {
1364-
bd.HTTP = bd.HTTP.WithContext(ctx)
1365-
})
1366-
1367-
// Verify error
1368-
require.Error(t, err)
1369-
require.Contains(t, err.Error(), defs.NewErrNoSufficientScope("me:delete").Error())
1370-
})
1371-
1372-
t.Run("user not found", func(t *testing.T) {
1373-
entClient := testhelper.NewEntSqliteClient(t)
1374-
1375-
// Setup database with proper groups and scopes
1376-
_, err := setup.Setup(context.Background(), entClient)
1377-
require.NoError(t, err)
1378-
1379-
resolver := &Resolver{
1380-
ent: entClient,
1381-
auth: &mockAuthStorage{},
1382-
}
1383-
1384-
// Create test server with scope directive
1385-
cfg := Config{
1386-
Resolvers: resolver,
1387-
Directives: DirectiveRoot{Scope: directive.ScopeDirective},
1388-
}
1389-
srv := handler.New(NewExecutableSchema(cfg))
1390-
srv.AddTransport(transport.POST{})
1391-
c := client.New(srv)
1392-
1393-
// Create context with authenticated user but non-existent user ID
1394-
ctx := auth.WithUser(context.Background(), auth.TokenInfo{
1395-
UserID: 999, // Non-existent user ID
1396-
Scopes: []string{"me:delete"},
1397-
})
1398-
1399-
// Execute mutation
1400-
var resp struct {
1401-
DeleteMe bool
1402-
}
1403-
err = c.Post(`mutation { deleteMe }`, &resp, func(bd *client.Request) {
1404-
bd.HTTP = bd.HTTP.WithContext(ctx)
1405-
})
1406-
1407-
// Verify error
1408-
require.Error(t, err)
1409-
require.Contains(t, err.Error(), useraccount.ErrUserNotFound.Error())
1410-
})
1411-
}
1412-
14131254
func TestMutationResolver_VerifyRegistration(t *testing.T) {
14141255
t.Run("success", func(t *testing.T) {
14151256
entClient := testhelper.NewEntSqliteClient(t)

httpapi/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22

33
Database Playground 大部分的 API 均以 GraphQL 形式提供 (`/query`),但部分為 BFF (Backend for Frontend) 設定的 Stateful Endpoints 則是以 HTTP API 進行設計,並以 `/api` 為開頭。
44

5+
> [!WARNING]
6+
> 注意 HTTP API 不會帶入 AuthMiddleware。如果你的 API 需要鑒權,請手動帶入 `auth.Middleware`
7+
58
- [認證](./auth):相關方法均列於 `/api/auth` 路徑底下。

httpapi/auth/README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@ Auth 端點提供適合供網頁應用程式使用的認證 API。
66

77
您可以使用 `POST /api/logout` 登出帳號。
88

9-
如果沒有 Auth Token 或者是 token 無效,會回傳如這種結構的 HTTP 401 錯誤:
10-
11-
```json
12-
{
13-
"error": "You should be logged in to logout.",
14-
}
15-
```
16-
179
如果 Token 撤回失敗,則會回傳 HTTP 500 錯誤並帶上錯誤資訊:
1810

1911
```json
@@ -25,6 +17,8 @@ Auth 端點提供適合供網頁應用程式使用的認證 API。
2517

2618
如果 Token 撤回成功,則回傳 HTTP 205 (Reset Content),此時您可以重新整理登入狀態。
2719

20+
如果沒有 Auth Token 或者是 token 無效,則依然回傳 HTTP 205。請引導使用者重新登入。
21+
2822
## Google 登入
2923

3024
如果您要觸發 Google 登入的流程,請前往 `GET /api/auth/google/login`。可以帶入 `redirect_uri` 參數來在登入完成後轉導到指定畫面。

0 commit comments

Comments
 (0)