diff --git a/.github/workflows/label-pr-based-on-paths.yml b/.github/workflows/label-pr-based-on-paths.yml index e077870..3c6d97a 100644 --- a/.github/workflows/label-pr-based-on-paths.yml +++ b/.github/workflows/label-pr-based-on-paths.yml @@ -28,9 +28,11 @@ jobs: const labelsToColor = { auth: 'f66a0a', // Orange utils: '1f77b4', // Blue + lib: '1f77b4', // Blue settings: '8c564b', // Brown dependency: '2ca02c', // Green db: '9467bd', // Purple + test: 'd62728', // Red }; const labels = new Set(); @@ -39,9 +41,15 @@ jobs: if (file.filename.startsWith('src/auth')) { labels.add('auth'); } + if (file.filename.endsWith('.test.ts')) { + labels.add('test'); + } if (file.filename.startsWith('src/utils')) { labels.add('utils'); } + if (file.filename.startsWith('src/lib')) { + labels.add('lib'); + } if (file.filename.startsWith('src/settings')) { labels.add('settings'); } diff --git a/README.md b/README.md index 87b7a58..f0af977 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,7 @@ > Kumo - 日语中的雲☁️ - 一个基于 Cloudflare Workers、D1 数据库和 Hono 框架构建的高效身份认证系统 -![Test by Github Action](https://img.shields.io/github/actions/workflow/status/ZL-Asica/KumoAuth/auto-test.yml?logo=github&label=Test) ![GitHub License](https://img.shields.io/github/license/ZL-Asica/KumoAuth) ![Yarn Version](https://img.shields.io/github/package-json/packageManager/ZL-Asica/KumoAuth?label=&logo=yarn&logoColor=fff) - -![Hono](https://img.shields.io/badge/Hono-E36002?logo=hono&logoColor=fff) ![Cloudflare](https://img.shields.io/badge/Cloudflare-F38020?logo=Cloudflare&logoColor=white) ![Eslint](https://img.shields.io/badge/eslint-4B32C3?logo=eslint&logoColor=white) ![Prettier](https://img.shields.io/badge/Prettier-F7B93E?logo=Prettier&logoColor=white) +![Test by Github Action](https://img.shields.io/github/actions/workflow/status/ZL-Asica/KumoAuth/auto-test.yml?logo=github&label=Test) ![GitHub License](https://img.shields.io/github/license/ZL-Asica/KumoAuth) ![Yarn Version](https://img.shields.io/github/package-json/packageManager/ZL-Asica/KumoAuth?label=&logo=yarn&logoColor=fff) | ![Hono](https://img.shields.io/badge/Hono-E36002?logo=hono&logoColor=fff) ![Cloudflare](https://img.shields.io/badge/Cloudflare-F38020?logo=Cloudflare&logoColor=white) ![Eslint](https://img.shields.io/badge/eslint-4B32C3?logo=eslint&logoColor=white) ![Prettier](https://img.shields.io/badge/Prettier-F7B93E?logo=Prettier&logoColor=white) 此项目旨在利用 Cloudflare 的无服务器架构搭建一个简单、轻量的身份认证系统。项目使用了 JWT 来实现用户的无状态认证和访问保护功能,未来计划加入更多功能,如双因素认证、刷新令牌等。 diff --git a/README_EN.md b/README_EN.md index 2e4d193..6e5d4ad 100644 --- a/README_EN.md +++ b/README_EN.md @@ -4,9 +4,7 @@ > Kumo - means cloud (雲☁️) in Japanese - is a lightweight and efficient authentication system built with Cloudflare Workers, D1 Database, and the Hono framework. -![Test by Github Action](https://img.shields.io/github/actions/workflow/status/ZL-Asica/KumoAuth/auto-test.yml?logo=github&label=Test) ![GitHub License](https://img.shields.io/github/license/ZL-Asica/KumoAuth) ![Yarn Version](https://img.shields.io/github/package-json/packageManager/ZL-Asica/KumoAuth?label=&logo=yarn&logoColor=fff) - -![Hono](https://img.shields.io/badge/Hono-E36002?logo=hono&logoColor=fff) ![Cloudflare](https://img.shields.io/badge/Cloudflare-F38020?logo=Cloudflare&logoColor=white) ![Eslint](https://img.shields.io/badge/eslint-4B32C3?logo=eslint&logoColor=white) ![Prettier](https://img.shields.io/badge/Prettier-F7B93E?logo=Prettier&logoColor=white) +![Test by Github Action](https://img.shields.io/github/actions/workflow/status/ZL-Asica/KumoAuth/auto-test.yml?logo=github&label=Test) ![GitHub License](https://img.shields.io/github/license/ZL-Asica/KumoAuth) ![Yarn Version](https://img.shields.io/github/package-json/packageManager/ZL-Asica/KumoAuth?label=&logo=yarn&logoColor=fff) | ![Hono](https://img.shields.io/badge/Hono-E36002?logo=hono&logoColor=fff) ![Cloudflare](https://img.shields.io/badge/Cloudflare-F38020?logo=Cloudflare&logoColor=white) ![Eslint](https://img.shields.io/badge/eslint-4B32C3?logo=eslint&logoColor=white) ![Prettier](https://img.shields.io/badge/Prettier-F7B93E?logo=Prettier&logoColor=white) This project leverages Cloudflare's serverless architecture to build a simple, lightweight authentication system. It uses JWTs for stateless authentication and access protection, with plans for additional features like two-factor authentication and refresh tokens. diff --git a/src/auth/status.test.ts b/src/auth/status.test.ts index 14698a7..928b707 100644 --- a/src/auth/status.test.ts +++ b/src/auth/status.test.ts @@ -1,4 +1,5 @@ import { authStatusHandler } from '@/auth/status' +import { getUserByUserId } from '@/lib/db' import type { Context } from 'hono' import { getSignedCookie } from 'hono/cookie' import { verify } from 'hono/jwt' @@ -7,10 +8,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' // Mock dependencies vi.mock('hono/cookie') vi.mock('hono/jwt') +vi.mock('@/lib/db') // Mock Context const mockContext = { - env: { JWT_SECRET: 'testSecret' }, + env: { JWT_SECRET: 'testSecret', DB: 'testDB' }, req: { json: vi.fn() }, json: vi.fn(), header: vi.fn(), @@ -21,10 +23,20 @@ describe('authStatusHandler', () => { vi.clearAllMocks() }) - it('should return 200 if user is logged in', async () => { - // Mock a valid token + it('should return 200 with user details if user is logged in', async () => { vi.mocked(getSignedCookie).mockResolvedValueOnce('valid.token') - vi.mocked(verify).mockResolvedValueOnce({ user_id: 1 }) + vi.mocked(verify).mockResolvedValueOnce({ + user_id: 1, + user_role_id: 1, + exp: 1234567890, + }) + vi.mocked(getUserByUserId).mockResolvedValueOnce({ + user_id: 1, + username: 'testUser', + user_role_id: 1, + password_hash: 'testHash', + created_at: '2023-11-01T12:00:00Z', + }) await authStatusHandler(mockContext) @@ -32,7 +44,16 @@ describe('authStatusHandler', () => { 'valid.token', mockContext.env.JWT_SECRET ) - expect(mockContext.json).toHaveBeenCalledWith({ message: 'Logged in' }, 200) + expect(getUserByUserId).toHaveBeenCalledWith(mockContext.env.DB, 1) + expect(mockContext.json).toHaveBeenCalledWith( + { + user_id: 1, + username: 'testUser', + user_role_id: 1, + created_at: '2023-11-01T12:00:00Z', + }, + 200 + ) }) it('should return 401 if user is not logged in (no token)', async () => { @@ -72,4 +93,17 @@ describe('authStatusHandler', () => { 403 ) }) + + it('should return 403 Token verification failed when a non-Error is thrown', async () => { + // Mock error + vi.mocked(getSignedCookie).mockResolvedValueOnce('invalid.token.test') + vi.mocked(verify).mockRejectedValueOnce('Token verification failed') + + await authStatusHandler(mockContext) + + expect(mockContext.json).toHaveBeenCalledWith( + { error: 'Token verification failed' }, + 403 + ) + }) }) diff --git a/src/auth/status.ts b/src/auth/status.ts index f43d220..62ecace 100644 --- a/src/auth/status.ts +++ b/src/auth/status.ts @@ -1,17 +1,24 @@ -import { errorResponse, jsonMessageContent } from '@/lib/helper' -import { createRoute } from '@hono/zod-openapi' +import { getUserByUserId } from '@/lib/db' +import { errorResponse, jsonContent } from '@/lib/helper' +import { createRoute, z } from '@hono/zod-openapi' import type { Context } from 'hono' import { getSignedCookie } from 'hono/cookie' import { verify } from 'hono/jwt' -// authStatusRoute +const authStatusSchema = z.object({ + user_id: z.number(), + username: z.string(), + user_role_id: z.number(), + created_at: z.string(), +}) + export const authStatusRoute = createRoute({ method: 'get', path: '/auth/status', responses: { - 200: jsonMessageContent('Logged in'), - 401: errorResponse('Not logged in or token expired'), - 403: errorResponse('Invalid token or token verification failed'), + 200: jsonContent(authStatusSchema, 'User details'), + 401: errorResponse('Token expired'), + 403: errorResponse('Invalid token'), }, }) @@ -23,8 +30,27 @@ export const authStatusHandler = async (c: Context) => { } try { - await verify(token, c.env.JWT_SECRET) - return c.json({ message: 'Logged in' }, 200) + const decoded = await verify(token, c.env.JWT_SECRET) + + if (!decoded || typeof decoded.user_id !== 'number') { + return c.json({ error: 'Invalid token' }, 403) + } + + const user = await getUserByUserId(c.env.DB, decoded.user_id) + + if (!user) { + return c.json({ error: 'User not found' }, 403) + } + + return c.json( + { + user_id: user.user_id, + username: user.username, + user_role_id: user.user_role_id, + created_at: user.created_at, + }, + 200 + ) } catch (error) { if (error instanceof Error) { if (error.message === 'TokenExpiredError') {