create-velox-app 0.4.7 → 0.4.9
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/dist/index.js.map +1 -1
- package/dist/templates/compiler.d.ts.map +1 -1
- package/dist/templates/compiler.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +9 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/placeholders.d.ts +5 -0
- package/dist/templates/placeholders.d.ts.map +1 -1
- package/dist/templates/placeholders.js +14 -3
- package/dist/templates/placeholders.js.map +1 -1
- package/dist/templates/shared/web-base.d.ts +1 -0
- package/dist/templates/shared/web-base.d.ts.map +1 -1
- package/dist/templates/shared/web-base.js +5 -0
- package/dist/templates/shared/web-base.js.map +1 -1
- package/dist/templates/trpc.d.ts +15 -0
- package/dist/templates/trpc.d.ts.map +1 -0
- package/dist/templates/trpc.js +89 -0
- package/dist/templates/trpc.js.map +1 -0
- package/dist/templates/types.d.ts +1 -1
- package/dist/templates/types.d.ts.map +1 -1
- package/dist/templates/types.js +6 -0
- package/dist/templates/types.js.map +1 -1
- package/package.json +1 -1
- package/src/templates/source/api/config/auth.ts +7 -0
- package/src/templates/source/api/index.auth.ts +9 -2
- package/src/templates/source/api/index.default.ts +2 -1
- package/src/templates/source/api/index.trpc.ts +64 -0
- package/src/templates/source/api/prisma.config.ts +1 -0
- package/src/templates/source/api/procedures/auth.ts +10 -4
- package/src/templates/source/api/procedures/health.ts +1 -1
- package/src/templates/source/api/procedures/users.auth.ts +24 -68
- package/src/templates/source/api/procedures/users.default.ts +28 -58
- package/src/templates/source/api/schemas/user.ts +9 -4
- package/src/templates/source/web/App.module.css +6 -2
- package/src/templates/source/web/api.ts +42 -0
- package/src/templates/source/web/main.tsx +43 -12
- package/src/templates/source/web/package.json +2 -1
- package/src/templates/source/web/routes/__root.tsx +24 -6
- package/src/templates/source/web/routes/about.tsx +8 -2
- package/src/templates/source/web/routes/index.auth.tsx +22 -24
- package/src/templates/source/web/routes/index.default.tsx +12 -18
- package/src/templates/source/web/routes/users.tsx +64 -49
- package/src/templates/source/web/vite.config.ts +4 -3
|
@@ -2,62 +2,25 @@
|
|
|
2
2
|
* User Procedures
|
|
3
3
|
*
|
|
4
4
|
* CRUD procedures for user management with authentication guards.
|
|
5
|
+
* Clean implementation without boilerplate:
|
|
6
|
+
* - No manual DbUser/DbClient interfaces needed
|
|
7
|
+
* - No toUserResponse() transformation needed
|
|
8
|
+
* - Output schema automatically serializes Date → string via withTimestamps()
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import {
|
|
8
12
|
AuthError,
|
|
9
13
|
authenticated,
|
|
10
|
-
hasRole,
|
|
11
14
|
defineProcedures,
|
|
12
15
|
GuardError,
|
|
13
|
-
|
|
16
|
+
hasRole,
|
|
17
|
+
NotFoundError,
|
|
14
18
|
paginationInputSchema,
|
|
19
|
+
procedure,
|
|
15
20
|
z,
|
|
16
21
|
} from '@veloxts/velox';
|
|
17
22
|
|
|
18
|
-
import {
|
|
19
|
-
CreateUserInput,
|
|
20
|
-
UpdateUserInput,
|
|
21
|
-
type User,
|
|
22
|
-
UserSchema,
|
|
23
|
-
} from '../schemas/user.js';
|
|
24
|
-
|
|
25
|
-
// ============================================================================
|
|
26
|
-
// Database Types
|
|
27
|
-
// ============================================================================
|
|
28
|
-
|
|
29
|
-
interface DbUser {
|
|
30
|
-
id: string;
|
|
31
|
-
name: string;
|
|
32
|
-
email: string;
|
|
33
|
-
createdAt: Date;
|
|
34
|
-
updatedAt: Date;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface DbClient {
|
|
38
|
-
user: {
|
|
39
|
-
findUnique: (args: { where: { id: string } }) => Promise<DbUser | null>;
|
|
40
|
-
findMany: (args?: { skip?: number; take?: number }) => Promise<DbUser[]>;
|
|
41
|
-
create: (args: { data: { name: string; email: string } }) => Promise<DbUser>;
|
|
42
|
-
update: (args: { where: { id: string }; data: { name?: string; email?: string } }) => Promise<DbUser>;
|
|
43
|
-
delete: (args: { where: { id: string } }) => Promise<DbUser>;
|
|
44
|
-
count: () => Promise<number>;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getDb(ctx: { db: unknown }): DbClient {
|
|
49
|
-
return ctx.db as DbClient;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function toUserResponse(dbUser: DbUser): User {
|
|
53
|
-
return {
|
|
54
|
-
id: dbUser.id,
|
|
55
|
-
name: dbUser.name,
|
|
56
|
-
email: dbUser.email,
|
|
57
|
-
createdAt: dbUser.createdAt instanceof Date ? dbUser.createdAt.toISOString() : dbUser.createdAt,
|
|
58
|
-
updatedAt: dbUser.updatedAt instanceof Date ? dbUser.updatedAt.toISOString() : dbUser.updatedAt,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
23
|
+
import { CreateUserInput, UpdateUserInput, UserSchema } from '../schemas/user.js';
|
|
61
24
|
|
|
62
25
|
// ============================================================================
|
|
63
26
|
// User Procedures
|
|
@@ -66,11 +29,14 @@ function toUserResponse(dbUser: DbUser): User {
|
|
|
66
29
|
export const userProcedures = defineProcedures('users', {
|
|
67
30
|
getUser: procedure()
|
|
68
31
|
.input(z.object({ id: z.string().uuid() }))
|
|
69
|
-
.output(UserSchema
|
|
32
|
+
.output(UserSchema)
|
|
70
33
|
.query(async ({ input, ctx }) => {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
34
|
+
const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw new NotFoundError(`User with id '${input.id}' not found`);
|
|
37
|
+
}
|
|
38
|
+
// Return Prisma object directly - output schema handles Date serialization
|
|
39
|
+
return user;
|
|
74
40
|
}),
|
|
75
41
|
|
|
76
42
|
listUsers: procedure()
|
|
@@ -86,20 +52,17 @@ export const userProcedures = defineProcedures('users', {
|
|
|
86
52
|
})
|
|
87
53
|
)
|
|
88
54
|
.query(async ({ input, ctx }) => {
|
|
89
|
-
const db = getDb(ctx);
|
|
90
55
|
const page = input?.page ?? 1;
|
|
91
56
|
const limit = input?.limit ?? 10;
|
|
92
57
|
const skip = (page - 1) * limit;
|
|
93
58
|
|
|
94
|
-
const [
|
|
95
|
-
db.user.findMany({ skip, take: limit }),
|
|
96
|
-
db.user.count(),
|
|
59
|
+
const [users, total] = await Promise.all([
|
|
60
|
+
ctx.db.user.findMany({ skip, take: limit }),
|
|
61
|
+
ctx.db.user.count(),
|
|
97
62
|
]);
|
|
98
63
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
meta: { page, limit, total },
|
|
102
|
-
};
|
|
64
|
+
// Return Prisma objects directly - output schema serializes dates
|
|
65
|
+
return { data: users, meta: { page, limit, total } };
|
|
103
66
|
}),
|
|
104
67
|
|
|
105
68
|
createUser: procedure()
|
|
@@ -107,9 +70,7 @@ export const userProcedures = defineProcedures('users', {
|
|
|
107
70
|
.input(CreateUserInput)
|
|
108
71
|
.output(UserSchema)
|
|
109
72
|
.mutation(async ({ input, ctx }) => {
|
|
110
|
-
|
|
111
|
-
const user = await db.user.create({ data: input });
|
|
112
|
-
return toUserResponse(user);
|
|
73
|
+
return ctx.db.user.create({ data: input });
|
|
113
74
|
}),
|
|
114
75
|
|
|
115
76
|
updateUser: procedure()
|
|
@@ -117,7 +78,6 @@ export const userProcedures = defineProcedures('users', {
|
|
|
117
78
|
.input(z.object({ id: z.string().uuid() }).merge(UpdateUserInput))
|
|
118
79
|
.output(UserSchema)
|
|
119
80
|
.mutation(async ({ input, ctx }) => {
|
|
120
|
-
const db = getDb(ctx);
|
|
121
81
|
const { id, ...data } = input;
|
|
122
82
|
|
|
123
83
|
if (!ctx.user) {
|
|
@@ -131,8 +91,7 @@ export const userProcedures = defineProcedures('users', {
|
|
|
131
91
|
throw new GuardError('ownership', 'You can only update your own profile', 403);
|
|
132
92
|
}
|
|
133
93
|
|
|
134
|
-
|
|
135
|
-
return toUserResponse(updated);
|
|
94
|
+
return ctx.db.user.update({ where: { id }, data });
|
|
136
95
|
}),
|
|
137
96
|
|
|
138
97
|
patchUser: procedure()
|
|
@@ -140,7 +99,6 @@ export const userProcedures = defineProcedures('users', {
|
|
|
140
99
|
.input(z.object({ id: z.string().uuid() }).merge(UpdateUserInput))
|
|
141
100
|
.output(UserSchema)
|
|
142
101
|
.mutation(async ({ input, ctx }) => {
|
|
143
|
-
const db = getDb(ctx);
|
|
144
102
|
const { id, ...data } = input;
|
|
145
103
|
|
|
146
104
|
if (!ctx.user) {
|
|
@@ -154,8 +112,7 @@ export const userProcedures = defineProcedures('users', {
|
|
|
154
112
|
throw new GuardError('ownership', 'You can only update your own profile', 403);
|
|
155
113
|
}
|
|
156
114
|
|
|
157
|
-
|
|
158
|
-
return toUserResponse(updated);
|
|
115
|
+
return ctx.db.user.update({ where: { id }, data });
|
|
159
116
|
}),
|
|
160
117
|
|
|
161
118
|
deleteUser: procedure()
|
|
@@ -163,8 +120,7 @@ export const userProcedures = defineProcedures('users', {
|
|
|
163
120
|
.input(z.object({ id: z.string().uuid() }))
|
|
164
121
|
.output(z.object({ success: z.boolean() }))
|
|
165
122
|
.mutation(async ({ input, ctx }) => {
|
|
166
|
-
|
|
167
|
-
await db.user.delete({ where: { id: input.id } });
|
|
123
|
+
await ctx.db.user.delete({ where: { id: input.id } });
|
|
168
124
|
return { success: true };
|
|
169
125
|
}),
|
|
170
126
|
});
|
|
@@ -1,53 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Procedures
|
|
3
|
+
*
|
|
4
|
+
* Clean implementation without boilerplate:
|
|
5
|
+
* - No manual DbUser/DbClient interfaces needed
|
|
6
|
+
* - No toUserResponse() transformation needed
|
|
7
|
+
* - Output schema automatically serializes Date → string via withTimestamps()
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
defineProcedures,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
paginationInputSchema,
|
|
14
|
+
procedure,
|
|
15
|
+
z,
|
|
16
|
+
} from '@veloxts/velox';
|
|
6
17
|
|
|
7
18
|
import { CreateUserInput, UpdateUserInput, UserSchema } from '../schemas/user.js';
|
|
8
19
|
|
|
9
|
-
// Database types
|
|
10
|
-
interface DbUser {
|
|
11
|
-
id: string;
|
|
12
|
-
name: string;
|
|
13
|
-
email: string;
|
|
14
|
-
createdAt: Date;
|
|
15
|
-
updatedAt: Date;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface DbClient {
|
|
19
|
-
user: {
|
|
20
|
-
findUnique: (args: { where: { id: string } }) => Promise<DbUser | null>;
|
|
21
|
-
findMany: (args?: { skip?: number; take?: number }) => Promise<DbUser[]>;
|
|
22
|
-
create: (args: { data: { name: string; email: string } }) => Promise<DbUser>;
|
|
23
|
-
update: (args: { where: { id: string }; data: { name?: string; email?: string } }) => Promise<DbUser>;
|
|
24
|
-
delete: (args: { where: { id: string } }) => Promise<DbUser>;
|
|
25
|
-
count: () => Promise<number>;
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getDb(ctx: { db: unknown }): DbClient {
|
|
30
|
-
return ctx.db as DbClient;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function toUserResponse(dbUser: DbUser) {
|
|
34
|
-
return {
|
|
35
|
-
id: dbUser.id,
|
|
36
|
-
name: dbUser.name,
|
|
37
|
-
email: dbUser.email,
|
|
38
|
-
createdAt: dbUser.createdAt.toISOString(),
|
|
39
|
-
updatedAt: dbUser.updatedAt.toISOString(),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
20
|
export const userProcedures = defineProcedures('users', {
|
|
44
21
|
getUser: procedure()
|
|
45
22
|
.input(z.object({ id: z.string().uuid() }))
|
|
46
|
-
.output(UserSchema
|
|
23
|
+
.output(UserSchema)
|
|
47
24
|
.query(async ({ input, ctx }) => {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
const user = await ctx.db.user.findUnique({ where: { id: input.id } });
|
|
26
|
+
if (!user) {
|
|
27
|
+
throw new NotFoundError(`User with id '${input.id}' not found`);
|
|
28
|
+
}
|
|
29
|
+
// Return Prisma object directly - output schema handles Date serialization
|
|
30
|
+
return user;
|
|
51
31
|
}),
|
|
52
32
|
|
|
53
33
|
listUsers: procedure()
|
|
@@ -63,57 +43,47 @@ export const userProcedures = defineProcedures('users', {
|
|
|
63
43
|
})
|
|
64
44
|
)
|
|
65
45
|
.query(async ({ input, ctx }) => {
|
|
66
|
-
const db = getDb(ctx);
|
|
67
46
|
const page = input?.page ?? 1;
|
|
68
47
|
const limit = input?.limit ?? 10;
|
|
69
48
|
const skip = (page - 1) * limit;
|
|
70
49
|
|
|
71
|
-
const [
|
|
72
|
-
db.user.findMany({ skip, take: limit }),
|
|
73
|
-
db.user.count(),
|
|
50
|
+
const [users, total] = await Promise.all([
|
|
51
|
+
ctx.db.user.findMany({ skip, take: limit }),
|
|
52
|
+
ctx.db.user.count(),
|
|
74
53
|
]);
|
|
75
54
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
meta: { page, limit, total },
|
|
79
|
-
};
|
|
55
|
+
// Return Prisma objects directly - output schema serializes dates
|
|
56
|
+
return { data: users, meta: { page, limit, total } };
|
|
80
57
|
}),
|
|
81
58
|
|
|
82
59
|
createUser: procedure()
|
|
83
60
|
.input(CreateUserInput)
|
|
84
61
|
.output(UserSchema)
|
|
85
62
|
.mutation(async ({ input, ctx }) => {
|
|
86
|
-
|
|
87
|
-
const user = await db.user.create({ data: input });
|
|
88
|
-
return toUserResponse(user);
|
|
63
|
+
return ctx.db.user.create({ data: input });
|
|
89
64
|
}),
|
|
90
65
|
|
|
91
66
|
updateUser: procedure()
|
|
92
67
|
.input(z.object({ id: z.string().uuid() }).merge(UpdateUserInput))
|
|
93
68
|
.output(UserSchema)
|
|
94
69
|
.mutation(async ({ input, ctx }) => {
|
|
95
|
-
const db = getDb(ctx);
|
|
96
70
|
const { id, ...data } = input;
|
|
97
|
-
|
|
98
|
-
return toUserResponse(user);
|
|
71
|
+
return ctx.db.user.update({ where: { id }, data });
|
|
99
72
|
}),
|
|
100
73
|
|
|
101
74
|
patchUser: procedure()
|
|
102
75
|
.input(z.object({ id: z.string().uuid() }).merge(UpdateUserInput))
|
|
103
76
|
.output(UserSchema)
|
|
104
77
|
.mutation(async ({ input, ctx }) => {
|
|
105
|
-
const db = getDb(ctx);
|
|
106
78
|
const { id, ...data } = input;
|
|
107
|
-
|
|
108
|
-
return toUserResponse(user);
|
|
79
|
+
return ctx.db.user.update({ where: { id }, data });
|
|
109
80
|
}),
|
|
110
81
|
|
|
111
82
|
deleteUser: procedure()
|
|
112
83
|
.input(z.object({ id: z.string().uuid() }))
|
|
113
84
|
.output(z.object({ success: z.boolean() }))
|
|
114
85
|
.mutation(async ({ input, ctx }) => {
|
|
115
|
-
|
|
116
|
-
await db.user.delete({ where: { id: input.id } });
|
|
86
|
+
await ctx.db.user.delete({ where: { id: input.id } });
|
|
117
87
|
return { success: true };
|
|
118
88
|
}),
|
|
119
89
|
});
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User Schemas
|
|
3
|
+
*
|
|
4
|
+
* Uses withTimestamps() for automatic Date → string serialization.
|
|
5
|
+
* No manual transformation needed in procedure handlers.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
import { createIdSchema, emailSchema, z } from '@veloxts/velox';
|
|
8
|
+
import { createIdSchema, emailSchema, withTimestamps, z } from '@veloxts/velox';
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
// Business fields only - timestamps added separately
|
|
11
|
+
const UserFields = z.object({
|
|
8
12
|
id: createIdSchema('uuid'),
|
|
9
13
|
name: z.string().min(1).max(100),
|
|
10
14
|
email: emailSchema,
|
|
11
|
-
createdAt: z.coerce.date().transform((d) => d.toISOString()),
|
|
12
|
-
updatedAt: z.coerce.date().transform((d) => d.toISOString()),
|
|
13
15
|
});
|
|
14
16
|
|
|
17
|
+
// Complete schema with automatic Date → string serialization
|
|
18
|
+
export const UserSchema = withTimestamps(UserFields);
|
|
19
|
+
|
|
15
20
|
export type User = z.infer<typeof UserSchema>;
|
|
16
21
|
|
|
17
22
|
export const CreateUserInput = z.object({
|
|
@@ -99,7 +99,9 @@
|
|
|
99
99
|
border: 1px solid #222;
|
|
100
100
|
border-radius: 12px;
|
|
101
101
|
padding: 1.5rem;
|
|
102
|
-
transition:
|
|
102
|
+
transition:
|
|
103
|
+
border-color 0.2s,
|
|
104
|
+
transform 0.2s;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
.card:hover {
|
|
@@ -217,7 +219,9 @@
|
|
|
217
219
|
font-size: 1rem;
|
|
218
220
|
font-weight: 600;
|
|
219
221
|
cursor: pointer;
|
|
220
|
-
transition:
|
|
222
|
+
transition:
|
|
223
|
+
opacity 0.2s,
|
|
224
|
+
transform 0.1s;
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
.button:hover {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VeloxTS API Hooks
|
|
3
|
+
*
|
|
4
|
+
* This file creates typed hooks for accessing your backend procedures.
|
|
5
|
+
* Import `api` in your components for full autocomplete support.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { api } from '@/api';
|
|
10
|
+
* import { useQueryClient } from '@veloxts/client/react';
|
|
11
|
+
*
|
|
12
|
+
* function UserProfile({ userId }: { userId: string }) {
|
|
13
|
+
* const queryClient = useQueryClient();
|
|
14
|
+
*
|
|
15
|
+
* const { data: user } = api.users.getUser.useQuery({ id: userId });
|
|
16
|
+
* const { mutate } = api.users.updateUser.useMutation({
|
|
17
|
+
* onSuccess: () => api.users.getUser.invalidate({ id: userId }, queryClient),
|
|
18
|
+
* });
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createVeloxHooks } from '@veloxts/client/react';
|
|
24
|
+
|
|
25
|
+
import type { AppRouter } from '../../api/src/index.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Type-safe API hooks with full autocomplete
|
|
29
|
+
*
|
|
30
|
+
* Hooks (call inside components):
|
|
31
|
+
* - api.namespace.procedure.useQuery(input, options)
|
|
32
|
+
* - api.namespace.procedure.useMutation(options)
|
|
33
|
+
* - api.namespace.procedure.useSuspenseQuery(input, options)
|
|
34
|
+
*
|
|
35
|
+
* Cache utilities (input first, queryClient last):
|
|
36
|
+
* - api.namespace.procedure.getQueryKey(input)
|
|
37
|
+
* - api.namespace.procedure.invalidate(input, queryClient)
|
|
38
|
+
* - api.namespace.procedure.prefetch(input, queryClient)
|
|
39
|
+
* - api.namespace.procedure.getData(input, queryClient)
|
|
40
|
+
* - api.namespace.procedure.setData(input, data, queryClient)
|
|
41
|
+
*/
|
|
42
|
+
export const api = createVeloxHooks<AppRouter>();
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import { createRouter, RouterProvider } from '@tanstack/react-router';
|
|
2
|
+
import { VeloxProvider } from '@veloxts/client/react';
|
|
1
3
|
import { StrictMode } from 'react';
|
|
2
4
|
import { createRoot } from 'react-dom/client';
|
|
3
|
-
|
|
4
|
-
import { VeloxProvider } from '@veloxts/client/react';
|
|
5
|
+
|
|
5
6
|
import { routeTree } from './routeTree.gen';
|
|
6
7
|
import './styles/global.css';
|
|
7
8
|
|
|
8
9
|
// Import router type from API for full type safety
|
|
9
10
|
import type { AppRouter } from '../../api/src/index.js';
|
|
11
|
+
/* @if auth */
|
|
12
|
+
// Import routes directly from backend - no manual duplication needed
|
|
13
|
+
import { routes } from '../../api/src/index.js';
|
|
14
|
+
|
|
15
|
+
/* @endif auth */
|
|
10
16
|
|
|
11
17
|
// Create router with route tree
|
|
12
18
|
const router = createRouter({ routeTree });
|
|
@@ -25,15 +31,33 @@ const getAuthHeaders = () => {
|
|
|
25
31
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
26
32
|
};
|
|
27
33
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
// Automatic token refresh on 401 responses
|
|
35
|
+
const handleUnauthorized = async (): Promise<boolean> => {
|
|
36
|
+
const refreshToken = localStorage.getItem('refreshToken');
|
|
37
|
+
if (!refreshToken) return false;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch('/api/auth/refresh', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ refreshToken }),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
// Refresh failed - clear tokens
|
|
48
|
+
localStorage.removeItem('token');
|
|
49
|
+
localStorage.removeItem('refreshToken');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = (await res.json()) as { accessToken: string; refreshToken: string };
|
|
54
|
+
localStorage.setItem('token', data.accessToken);
|
|
55
|
+
localStorage.setItem('refreshToken', data.refreshToken);
|
|
56
|
+
return true; // Retry the original request
|
|
57
|
+
} catch {
|
|
58
|
+
// Network error during refresh
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
37
61
|
};
|
|
38
62
|
/* @endif auth */
|
|
39
63
|
|
|
@@ -49,7 +73,14 @@ createRoot(rootElement).render(
|
|
|
49
73
|
</VeloxProvider>
|
|
50
74
|
{/* @endif default */}
|
|
51
75
|
{/* @if auth */}
|
|
52
|
-
<VeloxProvider<AppRouter>
|
|
76
|
+
<VeloxProvider<AppRouter>
|
|
77
|
+
config={{
|
|
78
|
+
baseUrl: '/api',
|
|
79
|
+
headers: getAuthHeaders,
|
|
80
|
+
routes,
|
|
81
|
+
onUnauthorized: handleUnauthorized,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
53
84
|
<RouterProvider router={router} />
|
|
54
85
|
</VeloxProvider>
|
|
55
86
|
{/* @endif auth */}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { createRootRoute,
|
|
2
|
-
import styles from '@/App.module.css';
|
|
1
|
+
import { createRootRoute, Link, Outlet } from '@tanstack/react-router';
|
|
3
2
|
/* @if auth */
|
|
4
3
|
import { useQuery } from '@veloxts/client/react';
|
|
4
|
+
|
|
5
5
|
import type { AppRouter } from '../../../api/src/index.js';
|
|
6
|
+
import styles from '@/App.module.css';
|
|
6
7
|
/* @endif auth */
|
|
7
8
|
|
|
8
9
|
export const Route = createRootRoute({
|
|
@@ -11,7 +12,12 @@ export const Route = createRootRoute({
|
|
|
11
12
|
|
|
12
13
|
function RootLayout() {
|
|
13
14
|
/* @if auth */
|
|
14
|
-
const { data: user } = useQuery<AppRouter, 'auth', 'getMe'>(
|
|
15
|
+
const { data: user } = useQuery<AppRouter, 'auth', 'getMe'>(
|
|
16
|
+
'auth',
|
|
17
|
+
'getMe',
|
|
18
|
+
{},
|
|
19
|
+
{ retry: false }
|
|
20
|
+
);
|
|
15
21
|
const isAuthenticated = !!user;
|
|
16
22
|
/* @endif auth */
|
|
17
23
|
|
|
@@ -28,18 +34,30 @@ function RootLayout() {
|
|
|
28
34
|
Home
|
|
29
35
|
</Link>
|
|
30
36
|
{/* @if default */}
|
|
31
|
-
<Link
|
|
37
|
+
<Link
|
|
38
|
+
to="/users"
|
|
39
|
+
className={styles.navLink}
|
|
40
|
+
activeProps={{ className: styles.navLinkActive }}
|
|
41
|
+
>
|
|
32
42
|
Users
|
|
33
43
|
</Link>
|
|
34
44
|
{/* @endif default */}
|
|
35
45
|
{/* @if auth */}
|
|
36
46
|
{isAuthenticated && (
|
|
37
|
-
<Link
|
|
47
|
+
<Link
|
|
48
|
+
to="/users"
|
|
49
|
+
className={styles.navLink}
|
|
50
|
+
activeProps={{ className: styles.navLinkActive }}
|
|
51
|
+
>
|
|
38
52
|
Users
|
|
39
53
|
</Link>
|
|
40
54
|
)}
|
|
41
55
|
{/* @endif auth */}
|
|
42
|
-
<Link
|
|
56
|
+
<Link
|
|
57
|
+
to="/about"
|
|
58
|
+
className={styles.navLink}
|
|
59
|
+
activeProps={{ className: styles.navLinkActive }}
|
|
60
|
+
>
|
|
43
61
|
About
|
|
44
62
|
</Link>
|
|
45
63
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router';
|
|
2
|
+
|
|
2
3
|
import styles from '@/App.module.css';
|
|
3
4
|
|
|
4
5
|
export const Route = createFileRoute('/about')({
|
|
@@ -18,12 +19,17 @@ function AboutPage() {
|
|
|
18
19
|
<div className={styles.cards}>
|
|
19
20
|
<div className={styles.card}>
|
|
20
21
|
<h2>Type Safety</h2>
|
|
21
|
-
<p>
|
|
22
|
+
<p>
|
|
23
|
+
End-to-end type safety without code generation. Types flow from backend to frontend
|
|
24
|
+
automatically.
|
|
25
|
+
</p>
|
|
22
26
|
</div>
|
|
23
27
|
|
|
24
28
|
<div className={styles.card}>
|
|
25
29
|
<h2>Developer Experience</h2>
|
|
26
|
-
<p>
|
|
30
|
+
<p>
|
|
31
|
+
Convention over configuration. Sensible defaults with escape hatches when you need them.
|
|
32
|
+
</p>
|
|
27
33
|
</div>
|
|
28
34
|
|
|
29
35
|
<div className={styles.card}>
|