@yoms/create-monorepo 2.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -4
- package/dist/index.js +113 -4
- package/package.json +1 -1
- package/templates/backend-hono/base/{AGENT.md → CLAUDE.md} +42 -3
- package/templates/backend-hono/features/better-auth/env-additions.txt +4 -0
- package/templates/backend-hono/features/better-auth/package-additions.json +5 -0
- package/templates/backend-hono/features/better-auth/prisma/schema.prisma +69 -0
- package/templates/backend-hono/features/better-auth/src/config/env-auth.ts +8 -0
- package/templates/backend-hono/features/better-auth/src/index.ts +53 -0
- package/templates/backend-hono/features/better-auth/src/lib/auth.ts +21 -0
- package/templates/backend-hono/features/better-auth/src/middleware/auth.middleware.ts +38 -0
- package/templates/frontend-nextjs/base/CLAUDE.md +183 -0
- package/templates/frontend-nextjs/base/app/layout.tsx +4 -1
- package/templates/frontend-nextjs/base/components/examples/users-list-example.tsx +127 -0
- package/templates/frontend-nextjs/base/lib/auth-client.ts +18 -0
- package/templates/frontend-nextjs/base/package.json +4 -1
- package/templates/frontend-nextjs/base/providers/query-provider.tsx +28 -0
- package/templates/frontend-nextjs/base/services/README.md +184 -0
- package/templates/frontend-nextjs/base/{lib/api-client.ts → services/api/client.ts} +1 -0
- package/templates/frontend-nextjs/base/services/api/endpoints.ts +26 -0
- package/templates/frontend-nextjs/base/services/api/index.ts +6 -0
- package/templates/frontend-nextjs/base/services/auth/auth.hook.ts +61 -0
- package/templates/frontend-nextjs/base/services/auth/auth.types.ts +38 -0
- package/templates/frontend-nextjs/base/services/auth/index.ts +12 -0
- package/templates/frontend-nextjs/base/services/users/index.ts +8 -0
- package/templates/frontend-nextjs/base/services/users/users.hook.ts +119 -0
- package/templates/frontend-nextjs/base/services/users/users.queries.ts +14 -0
- package/templates/frontend-nextjs/base/services/users/users.service.ts +65 -0
- package/templates/frontend-nextjs/base/services/users/users.types.ts +37 -0
- package/templates/shared/base/CLAUDE.md +95 -0
- package/templates/backend-hono/features/jwt-auth/env-additions.txt +0 -5
- package/templates/backend-hono/features/jwt-auth/package-additions.json +0 -10
- package/templates/backend-hono/features/jwt-auth/src/config/env-additions.ts +0 -16
- package/templates/backend-hono/features/jwt-auth/src/lib/jwt.ts +0 -75
- package/templates/backend-hono/features/jwt-auth/src/middleware/auth.middleware.ts +0 -50
- package/templates/backend-hono/features/jwt-auth/src/routes/auth.route.ts +0 -157
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth types — aligned with better-auth's session model.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface AuthUser {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
email: string;
|
|
9
|
+
emailVerified: boolean;
|
|
10
|
+
image?: string | null;
|
|
11
|
+
createdAt: Date;
|
|
12
|
+
updatedAt: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AuthSession {
|
|
16
|
+
user: AuthUser;
|
|
17
|
+
session: {
|
|
18
|
+
id: string;
|
|
19
|
+
userId: string;
|
|
20
|
+
expiresAt: Date;
|
|
21
|
+
token: string;
|
|
22
|
+
ipAddress?: string | null;
|
|
23
|
+
userAgent?: string | null;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SignInInput {
|
|
28
|
+
email: string;
|
|
29
|
+
password: string;
|
|
30
|
+
callbackURL?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SignUpInput {
|
|
34
|
+
name: string;
|
|
35
|
+
email: string;
|
|
36
|
+
password: string;
|
|
37
|
+
callbackURL?: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth service exports
|
|
3
|
+
*
|
|
4
|
+
* Quick start:
|
|
5
|
+
* import { useSession, useSignIn, useSignUp, useSignOut } from '@/services/auth';
|
|
6
|
+
*
|
|
7
|
+
* For direct better-auth client access:
|
|
8
|
+
* import { authClient } from '@/lib/auth-client';
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from './auth.types';
|
|
12
|
+
export * from './auth.hook';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User React Query hooks
|
|
3
|
+
* Custom hooks that wrap user service calls with React Query
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
useQuery,
|
|
8
|
+
useMutation,
|
|
9
|
+
useQueryClient,
|
|
10
|
+
type UseQueryOptions,
|
|
11
|
+
type UseMutationOptions,
|
|
12
|
+
} from '@tanstack/react-query';
|
|
13
|
+
import { UserService } from './users.service';
|
|
14
|
+
import { userKeys } from './users.queries';
|
|
15
|
+
import type {
|
|
16
|
+
User,
|
|
17
|
+
PaginatedUsers,
|
|
18
|
+
CreateUserInput,
|
|
19
|
+
UpdateUserInput,
|
|
20
|
+
UsersListParams,
|
|
21
|
+
} from './users.types';
|
|
22
|
+
import type { ApiResponse } from '__PACKAGE_SCOPE__/shared';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Hook to fetch paginated users list
|
|
26
|
+
*/
|
|
27
|
+
export function useUsers(
|
|
28
|
+
params?: UsersListParams,
|
|
29
|
+
options?: Omit<
|
|
30
|
+
UseQueryOptions<ApiResponse<PaginatedUsers>>,
|
|
31
|
+
'queryKey' | 'queryFn'
|
|
32
|
+
>
|
|
33
|
+
) {
|
|
34
|
+
return useQuery({
|
|
35
|
+
queryKey: userKeys.list(params),
|
|
36
|
+
queryFn: () => UserService.getUsers(params),
|
|
37
|
+
...options,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hook to fetch a single user by ID
|
|
43
|
+
*/
|
|
44
|
+
export function useUser(
|
|
45
|
+
id: string,
|
|
46
|
+
options?: Omit<UseQueryOptions<ApiResponse<User>>, 'queryKey' | 'queryFn'>
|
|
47
|
+
) {
|
|
48
|
+
return useQuery({
|
|
49
|
+
queryKey: userKeys.detail(id),
|
|
50
|
+
queryFn: () => UserService.getUserById(id),
|
|
51
|
+
enabled: !!id,
|
|
52
|
+
...options,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Hook to create a new user
|
|
58
|
+
*/
|
|
59
|
+
export function useCreateUser(
|
|
60
|
+
options?: UseMutationOptions<
|
|
61
|
+
ApiResponse<User>,
|
|
62
|
+
Error,
|
|
63
|
+
CreateUserInput
|
|
64
|
+
>
|
|
65
|
+
) {
|
|
66
|
+
const queryClient = useQueryClient();
|
|
67
|
+
|
|
68
|
+
return useMutation({
|
|
69
|
+
mutationFn: (data: CreateUserInput) => UserService.createUser(data),
|
|
70
|
+
onSuccess: () => {
|
|
71
|
+
// Invalidate users list to refetch after creation
|
|
72
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
73
|
+
},
|
|
74
|
+
...options,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Hook to update an existing user
|
|
80
|
+
*/
|
|
81
|
+
export function useUpdateUser(
|
|
82
|
+
options?: UseMutationOptions<
|
|
83
|
+
ApiResponse<User>,
|
|
84
|
+
Error,
|
|
85
|
+
{ id: string; data: UpdateUserInput }
|
|
86
|
+
>
|
|
87
|
+
) {
|
|
88
|
+
const queryClient = useQueryClient();
|
|
89
|
+
|
|
90
|
+
return useMutation({
|
|
91
|
+
mutationFn: ({ id, data }: { id: string; data: UpdateUserInput }) =>
|
|
92
|
+
UserService.updateUser(id, data),
|
|
93
|
+
onSuccess: (_, variables) => {
|
|
94
|
+
// Invalidate both the specific user and the users list
|
|
95
|
+
queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) });
|
|
96
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
97
|
+
},
|
|
98
|
+
...options,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Hook to delete a user
|
|
104
|
+
*/
|
|
105
|
+
export function useDeleteUser(
|
|
106
|
+
options?: UseMutationOptions<ApiResponse<void>, Error, string>
|
|
107
|
+
) {
|
|
108
|
+
const queryClient = useQueryClient();
|
|
109
|
+
|
|
110
|
+
return useMutation({
|
|
111
|
+
mutationFn: (id: string) => UserService.deleteUser(id),
|
|
112
|
+
onSuccess: (_, id) => {
|
|
113
|
+
// Remove the deleted user from cache and invalidate lists
|
|
114
|
+
queryClient.removeQueries({ queryKey: userKeys.detail(id) });
|
|
115
|
+
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
|
|
116
|
+
},
|
|
117
|
+
...options,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User query key factory
|
|
3
|
+
* Provides type-safe query keys for React Query
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { UsersListParams } from './users.types';
|
|
7
|
+
|
|
8
|
+
export const userKeys = {
|
|
9
|
+
all: ['users'] as const,
|
|
10
|
+
lists: () => [...userKeys.all, 'list'] as const,
|
|
11
|
+
list: (params?: UsersListParams) => [...userKeys.lists(), params] as const,
|
|
12
|
+
details: () => [...userKeys.all, 'detail'] as const,
|
|
13
|
+
detail: (id: string) => [...userKeys.details(), id] as const,
|
|
14
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User service
|
|
3
|
+
* Service layer for user-related API calls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ApiClient } from '../api/client';
|
|
7
|
+
import { API_ENDPOINTS } from '../api/endpoints';
|
|
8
|
+
import type {
|
|
9
|
+
User,
|
|
10
|
+
PaginatedUsers,
|
|
11
|
+
CreateUserInput,
|
|
12
|
+
UpdateUserInput,
|
|
13
|
+
UsersListParams,
|
|
14
|
+
} from './users.types';
|
|
15
|
+
import type { ApiResponse } from '__PACKAGE_SCOPE__/shared';
|
|
16
|
+
|
|
17
|
+
export class UserService {
|
|
18
|
+
/**
|
|
19
|
+
* Fetch paginated list of users
|
|
20
|
+
*/
|
|
21
|
+
static async getUsers(
|
|
22
|
+
params?: UsersListParams
|
|
23
|
+
): Promise<ApiResponse<PaginatedUsers>> {
|
|
24
|
+
const searchParams = new URLSearchParams();
|
|
25
|
+
if (params?.page) searchParams.set('page', params.page.toString());
|
|
26
|
+
if (params?.limit) searchParams.set('limit', params.limit.toString());
|
|
27
|
+
if (params?.search) searchParams.set('search', params.search);
|
|
28
|
+
|
|
29
|
+
const query = searchParams.toString();
|
|
30
|
+
const endpoint = query ? `${API_ENDPOINTS.users.list}?${query}` : API_ENDPOINTS.users.list;
|
|
31
|
+
|
|
32
|
+
return ApiClient.get<PaginatedUsers>(endpoint);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetch a single user by ID
|
|
37
|
+
*/
|
|
38
|
+
static async getUserById(id: string): Promise<ApiResponse<User>> {
|
|
39
|
+
return ApiClient.get<User>(API_ENDPOINTS.users.byId(id));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a new user
|
|
44
|
+
*/
|
|
45
|
+
static async createUser(data: CreateUserInput): Promise<ApiResponse<User>> {
|
|
46
|
+
return ApiClient.post<User>(API_ENDPOINTS.users.create, data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Update an existing user
|
|
51
|
+
*/
|
|
52
|
+
static async updateUser(
|
|
53
|
+
id: string,
|
|
54
|
+
data: UpdateUserInput
|
|
55
|
+
): Promise<ApiResponse<User>> {
|
|
56
|
+
return ApiClient.put<User>(API_ENDPOINTS.users.update(id), data);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Delete a user
|
|
61
|
+
*/
|
|
62
|
+
static async deleteUser(id: string): Promise<ApiResponse<void>> {
|
|
63
|
+
return ApiClient.delete<void>(API_ENDPOINTS.users.delete(id));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User service types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
email: string;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CreateUserInput {
|
|
14
|
+
name: string;
|
|
15
|
+
email: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UpdateUserInput {
|
|
19
|
+
name?: string;
|
|
20
|
+
email?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UsersListParams {
|
|
24
|
+
page?: number;
|
|
25
|
+
limit?: number;
|
|
26
|
+
search?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PaginatedUsers {
|
|
30
|
+
data: User[];
|
|
31
|
+
meta: {
|
|
32
|
+
total: number;
|
|
33
|
+
page: number;
|
|
34
|
+
limit: number;
|
|
35
|
+
totalPages: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# CLAUDE.md — Shared Package (`__PACKAGE_SCOPE__/shared`)
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code when working in this package.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Shared TypeScript types and Zod schemas used by both the backend API and frontend web app. This is the single source of truth for data shapes across the monorepo.
|
|
8
|
+
|
|
9
|
+
## Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/
|
|
13
|
+
├── index.ts # Barrel export — everything exported from here
|
|
14
|
+
├── types.ts # Shared TypeScript interfaces (ApiResponse, PaginatedResponse)
|
|
15
|
+
└── schemas/
|
|
16
|
+
└── user.schema.ts # Zod schemas + inferred types for User domain
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Development Commands
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm build # Build (required before other packages can consume it)
|
|
23
|
+
pnpm typecheck # Type check
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
> **Important**: Run `pnpm build` after changes so backend and frontend pick up the latest types.
|
|
27
|
+
|
|
28
|
+
## Adding New Schemas
|
|
29
|
+
|
|
30
|
+
Create a new file in `src/schemas/`:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// src/schemas/post.schema.ts
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
|
|
36
|
+
export const PostSchema = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
title: z.string(),
|
|
39
|
+
content: z.string().nullable(),
|
|
40
|
+
authorId: z.string(),
|
|
41
|
+
createdAt: z.coerce.date(),
|
|
42
|
+
updatedAt: z.coerce.date(),
|
|
43
|
+
});
|
|
44
|
+
export type Post = z.infer<typeof PostSchema>;
|
|
45
|
+
|
|
46
|
+
export const CreatePostSchema = z.object({
|
|
47
|
+
title: z.string().min(1),
|
|
48
|
+
content: z.string().optional(),
|
|
49
|
+
authorId: z.string(),
|
|
50
|
+
});
|
|
51
|
+
export type CreatePost = z.infer<typeof CreatePostSchema>;
|
|
52
|
+
|
|
53
|
+
export const UpdatePostSchema = CreatePostSchema.partial();
|
|
54
|
+
export type UpdatePost = z.infer<typeof UpdatePostSchema>;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then re-export from `src/index.ts`:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
export * from './schemas/post.schema.js';
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Consuming in Backend
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { PostSchema, type CreatePost } from '__PACKAGE_SCOPE__/shared';
|
|
67
|
+
import { zValidator } from '@hono/zod-validator';
|
|
68
|
+
|
|
69
|
+
posts.post('/', zValidator('json', CreatePostSchema), async (c) => {
|
|
70
|
+
const data = c.req.valid('json'); // typed as CreatePost
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Consuming in Frontend
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import type { Post, CreatePost } from '__PACKAGE_SCOPE__/shared';
|
|
78
|
+
import { PostSchema } from '__PACKAGE_SCOPE__/shared';
|
|
79
|
+
|
|
80
|
+
// In service
|
|
81
|
+
export class PostService {
|
|
82
|
+
static async getAll(): Promise<ApiResponse<Post[]>> {
|
|
83
|
+
return ApiClient.get('/posts');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Rules
|
|
89
|
+
|
|
90
|
+
- Only pure TypeScript / Zod — no framework-specific code
|
|
91
|
+
- No runtime dependencies other than `zod`
|
|
92
|
+
- Always export both the Zod schema AND the inferred TypeScript type
|
|
93
|
+
- Schema names: `UserSchema` / Type names: `User` (no suffix)
|
|
94
|
+
- Use `.nullable()` not `.optional()` for fields that can be null in the DB
|
|
95
|
+
- Use `.coerce.date()` for `DateTime` fields from Prisma
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Add these to your env schema validation
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
export const jwtEnvSchema = z.object({
|
|
6
|
-
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
|
|
7
|
-
JWT_EXPIRES_IN: z.string().default('15m'),
|
|
8
|
-
JWT_REFRESH_SECRET: z.string().min(32, 'JWT_REFRESH_SECRET must be at least 32 characters'),
|
|
9
|
-
JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
// Merge this with your existing env schema:
|
|
13
|
-
// const envSchema = z.object({
|
|
14
|
-
// ...existingFields,
|
|
15
|
-
// ...jwtEnvSchema.shape,
|
|
16
|
-
// });
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import jwt from 'jsonwebtoken';
|
|
2
|
-
import { env } from '../config/env.js';
|
|
3
|
-
import { UnauthorizedError } from '../lib/errors.js';
|
|
4
|
-
|
|
5
|
-
export interface JwtPayload {
|
|
6
|
-
userId: string;
|
|
7
|
-
email: string;
|
|
8
|
-
type: 'access' | 'refresh';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface TokenPair {
|
|
12
|
-
accessToken: string;
|
|
13
|
-
refreshToken: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Generate access and refresh tokens for a user
|
|
18
|
-
*/
|
|
19
|
-
export function generateTokens(userId: string, email: string): TokenPair {
|
|
20
|
-
const accessToken = jwt.sign(
|
|
21
|
-
{ userId, email, type: 'access' } as JwtPayload,
|
|
22
|
-
env.JWT_SECRET,
|
|
23
|
-
{ expiresIn: env.JWT_EXPIRES_IN }
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
const refreshToken = jwt.sign(
|
|
27
|
-
{ userId, email, type: 'refresh' } as JwtPayload,
|
|
28
|
-
env.JWT_REFRESH_SECRET,
|
|
29
|
-
{ expiresIn: env.JWT_REFRESH_EXPIRES_IN }
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
return { accessToken, refreshToken };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Verify and decode an access token
|
|
37
|
-
*/
|
|
38
|
-
export function verifyAccessToken(token: string): JwtPayload {
|
|
39
|
-
try {
|
|
40
|
-
const decoded = jwt.verify(token, env.JWT_SECRET) as JwtPayload;
|
|
41
|
-
if (decoded.type !== 'access') {
|
|
42
|
-
throw new UnauthorizedError('Invalid token type');
|
|
43
|
-
}
|
|
44
|
-
return decoded;
|
|
45
|
-
} catch (error) {
|
|
46
|
-
if (error instanceof jwt.TokenExpiredError) {
|
|
47
|
-
throw new UnauthorizedError('Token expired');
|
|
48
|
-
}
|
|
49
|
-
if (error instanceof jwt.JsonWebTokenError) {
|
|
50
|
-
throw new UnauthorizedError('Invalid token');
|
|
51
|
-
}
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Verify and decode a refresh token
|
|
58
|
-
*/
|
|
59
|
-
export function verifyRefreshToken(token: string): JwtPayload {
|
|
60
|
-
try {
|
|
61
|
-
const decoded = jwt.verify(token, env.JWT_REFRESH_SECRET) as JwtPayload;
|
|
62
|
-
if (decoded.type !== 'refresh') {
|
|
63
|
-
throw new UnauthorizedError('Invalid token type');
|
|
64
|
-
}
|
|
65
|
-
return decoded;
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (error instanceof jwt.TokenExpiredError) {
|
|
68
|
-
throw new UnauthorizedError('Refresh token expired');
|
|
69
|
-
}
|
|
70
|
-
if (error instanceof jwt.JsonWebTokenError) {
|
|
71
|
-
throw new UnauthorizedError('Invalid refresh token');
|
|
72
|
-
}
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { Context, Next } from 'hono';
|
|
2
|
-
import { verifyAccessToken } from '../lib/jwt.js';
|
|
3
|
-
import { UnauthorizedError } from '../lib/errors.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Middleware to authenticate requests using JWT
|
|
7
|
-
* Extracts and verifies the JWT token from the Authorization header
|
|
8
|
-
* and attaches the decoded payload to c.get('jwtPayload')
|
|
9
|
-
*/
|
|
10
|
-
export async function authMiddleware(c: Context, next: Next): Promise<void> {
|
|
11
|
-
const authHeader = c.req.header('Authorization');
|
|
12
|
-
|
|
13
|
-
if (!authHeader) {
|
|
14
|
-
throw new UnauthorizedError('No authorization header');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const [type, token] = authHeader.split(' ');
|
|
18
|
-
|
|
19
|
-
if (type !== 'Bearer' || !token) {
|
|
20
|
-
throw new UnauthorizedError('Invalid authorization header format');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const payload = verifyAccessToken(token);
|
|
24
|
-
c.set('jwtPayload', payload);
|
|
25
|
-
|
|
26
|
-
await next();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Optional authentication middleware
|
|
31
|
-
* Similar to authMiddleware but doesn't throw if no token is present
|
|
32
|
-
* Use this for routes that work for both authenticated and unauthenticated users
|
|
33
|
-
*/
|
|
34
|
-
export async function optionalAuthMiddleware(c: Context, next: Next): Promise<void> {
|
|
35
|
-
const authHeader = c.req.header('Authorization');
|
|
36
|
-
|
|
37
|
-
if (authHeader) {
|
|
38
|
-
try {
|
|
39
|
-
const [type, token] = authHeader.split(' ');
|
|
40
|
-
if (type === 'Bearer' && token) {
|
|
41
|
-
const payload = verifyAccessToken(token);
|
|
42
|
-
c.set('jwtPayload', payload);
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// Silently ignore invalid tokens for optional auth
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
await next();
|
|
50
|
-
}
|