create-tigra 2.5.0 → 2.6.5
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/bin/create-tigra.js +5 -2
- package/package.json +1 -1
- package/template/_claude/commands/create-client.md +1 -1
- package/template/_claude/rules/client/01-project-structure.md +8 -0
- package/template/_claude/rules/client/04-design-system.md +162 -45
- package/template/_claude/rules/client/core.md +8 -6
- package/template/_claude/rules/global/completion-reports.md +178 -0
- package/template/_claude/rules/global/core.md +6 -0
- package/template/client/.env.example.production +5 -0
- package/template/client/README.md +1 -1
- package/template/client/src/app/error.tsx +1 -1
- package/template/client/src/app/globals.css +33 -85
- package/template/client/src/app/layout.tsx +6 -6
- package/template/client/src/app/page.tsx +10 -3
- package/template/client/src/components/common/ThemeSwitcher.tsx +112 -0
- package/template/client/src/components/layout/Header.tsx +1 -1
- package/template/client/src/features/auth/components/AuthInitializer.tsx +1 -1
- package/template/client/src/features/auth/components/LoginForm.tsx +1 -1
- package/template/client/src/features/auth/components/RegisterForm.tsx +1 -1
- package/template/client/src/features/auth/services/auth.service.ts +0 -4
- package/template/client/src/lib/constants/api-endpoints.ts +0 -2
- package/template/client/src/styles/fonts/inter-jetbrains.css +16 -0
- package/template/client/src/styles/themes/electric-indigo.css +90 -0
- package/template/client/src/styles/themes/ocean-teal.css +90 -0
- package/template/client/src/styles/themes/rose-pink.css +90 -0
- package/template/client/src/styles/themes/warm-orange.css +90 -0
- package/template/server/prisma/schema.prisma +0 -1
- package/template/server/src/modules/admin/admin.controller.ts +3 -9
- package/template/server/src/modules/admin/admin.routes.ts +7 -0
- package/template/server/src/modules/admin/admin.schemas.ts +13 -0
- package/template/server/src/modules/auth/auth.repo.ts +9 -8
- package/template/server/postman_collection.json +0 -666
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Theme: Warm Orange
|
|
3
|
+
* Accent: Warm terracotta orange — inspired by Claude's palette
|
|
4
|
+
* Vibe: Earthy, warm, approachable
|
|
5
|
+
*
|
|
6
|
+
* To activate: in globals.css, set @import "../styles/themes/warm-orange.css";
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
:root {
|
|
10
|
+
--radius: 0.625rem;
|
|
11
|
+
/* Warm cream background with terracotta orange accent */
|
|
12
|
+
--background: oklch(0.964 0.007 97.5);
|
|
13
|
+
--foreground: oklch(0.205 0.015 92);
|
|
14
|
+
--card: oklch(1 0 0);
|
|
15
|
+
--card-foreground: oklch(0.205 0.015 92);
|
|
16
|
+
--popover: oklch(1 0 0);
|
|
17
|
+
--popover-foreground: oklch(0.205 0.015 92);
|
|
18
|
+
--primary: oklch(0.597 0.135 39.9);
|
|
19
|
+
--primary-foreground: oklch(1 0 0);
|
|
20
|
+
--secondary: oklch(0.935 0.009 97.5);
|
|
21
|
+
--secondary-foreground: oklch(0.3 0.015 92);
|
|
22
|
+
--muted: oklch(0.935 0.009 97.5);
|
|
23
|
+
--muted-foreground: oklch(0.748 0.017 91.6);
|
|
24
|
+
--accent: oklch(0.935 0.009 97.5);
|
|
25
|
+
--accent-foreground: oklch(0.3 0.015 92);
|
|
26
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
27
|
+
--border: oklch(0.895 0.012 97.5);
|
|
28
|
+
--input: oklch(0.895 0.012 97.5);
|
|
29
|
+
--ring: oklch(0.597 0.135 39.9);
|
|
30
|
+
--success: oklch(0.52 0.17 155);
|
|
31
|
+
--success-foreground: oklch(1 0 0);
|
|
32
|
+
--warning: oklch(0.75 0.15 70);
|
|
33
|
+
--warning-foreground: oklch(0.25 0.015 92);
|
|
34
|
+
--info: oklch(0.55 0.15 240);
|
|
35
|
+
--info-foreground: oklch(1 0 0);
|
|
36
|
+
--chart-1: oklch(0.597 0.135 39.9);
|
|
37
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
38
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
39
|
+
--chart-4: oklch(0.828 0.12 84);
|
|
40
|
+
--chart-5: oklch(0.748 0.017 91.6);
|
|
41
|
+
--sidebar: oklch(0.95 0.007 97.5);
|
|
42
|
+
--sidebar-foreground: oklch(0.205 0.015 92);
|
|
43
|
+
--sidebar-primary: oklch(0.3 0.015 92);
|
|
44
|
+
--sidebar-primary-foreground: oklch(0.964 0.007 97.5);
|
|
45
|
+
--sidebar-accent: oklch(0.935 0.009 97.5);
|
|
46
|
+
--sidebar-accent-foreground: oklch(0.3 0.015 92);
|
|
47
|
+
--sidebar-border: oklch(0.895 0.012 97.5);
|
|
48
|
+
--sidebar-ring: oklch(0.597 0.135 39.9);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dark {
|
|
52
|
+
/* Dark mode: inverted with same warm undertone */
|
|
53
|
+
--background: oklch(0.185 0.012 92);
|
|
54
|
+
--foreground: oklch(0.93 0.007 97.5);
|
|
55
|
+
--card: oklch(0.235 0.012 92);
|
|
56
|
+
--card-foreground: oklch(0.93 0.007 97.5);
|
|
57
|
+
--popover: oklch(0.235 0.012 92);
|
|
58
|
+
--popover-foreground: oklch(0.93 0.007 97.5);
|
|
59
|
+
--primary: oklch(0.66 0.135 39.9);
|
|
60
|
+
--primary-foreground: oklch(0.185 0.012 92);
|
|
61
|
+
--secondary: oklch(0.28 0.012 92);
|
|
62
|
+
--secondary-foreground: oklch(0.93 0.007 97.5);
|
|
63
|
+
--muted: oklch(0.28 0.012 92);
|
|
64
|
+
--muted-foreground: oklch(0.65 0.015 91.6);
|
|
65
|
+
--accent: oklch(0.28 0.012 92);
|
|
66
|
+
--accent-foreground: oklch(0.93 0.007 97.5);
|
|
67
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
68
|
+
--border: oklch(1 0.007 97.5 / 12%);
|
|
69
|
+
--input: oklch(1 0.007 97.5 / 15%);
|
|
70
|
+
--ring: oklch(0.66 0.135 39.9);
|
|
71
|
+
--success: oklch(0.6 0.17 155);
|
|
72
|
+
--success-foreground: oklch(1 0 0);
|
|
73
|
+
--warning: oklch(0.8 0.15 70);
|
|
74
|
+
--warning-foreground: oklch(0.25 0.015 92);
|
|
75
|
+
--info: oklch(0.65 0.15 240);
|
|
76
|
+
--info-foreground: oklch(1 0 0);
|
|
77
|
+
--chart-1: oklch(0.66 0.135 39.9);
|
|
78
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
79
|
+
--chart-3: oklch(0.769 0.12 70);
|
|
80
|
+
--chart-4: oklch(0.627 0.18 303.9);
|
|
81
|
+
--chart-5: oklch(0.645 0.17 16.439);
|
|
82
|
+
--sidebar: oklch(0.235 0.012 92);
|
|
83
|
+
--sidebar-foreground: oklch(0.93 0.007 97.5);
|
|
84
|
+
--sidebar-primary: oklch(0.66 0.135 39.9);
|
|
85
|
+
--sidebar-primary-foreground: oklch(0.93 0.007 97.5);
|
|
86
|
+
--sidebar-accent: oklch(0.28 0.012 92);
|
|
87
|
+
--sidebar-accent-foreground: oklch(0.93 0.007 97.5);
|
|
88
|
+
--sidebar-border: oklch(1 0.007 97.5 / 12%);
|
|
89
|
+
--sidebar-ring: oklch(0.66 0.135 39.9);
|
|
90
|
+
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import { successResponse } from '@shared/responses/successResponse.js';
|
|
4
3
|
import { ValidationError } from '@shared/errors/errors.js';
|
|
5
4
|
import { blockIp, unblockIp, getBlockedIps } from '@libs/ip-block.js';
|
|
5
|
+
import { blockIpSchema } from './admin.schemas.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
ip: z.union([z.ipv4(), z.ipv6()], { message: 'Invalid IP address' }),
|
|
9
|
-
reason: z.string().max(500).optional(),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
type BlockIpBody = z.infer<typeof blockIpSchema>;
|
|
13
|
-
type UnblockIpParams = { ip: string };
|
|
7
|
+
import type { BlockIpInput, UnblockIpParams } from './admin.schemas.js';
|
|
14
8
|
|
|
15
9
|
export async function listBlockedIps(
|
|
16
10
|
_request: FastifyRequest,
|
|
@@ -21,7 +15,7 @@ export async function listBlockedIps(
|
|
|
21
15
|
}
|
|
22
16
|
|
|
23
17
|
export async function blockIpHandler(
|
|
24
|
-
request: FastifyRequest<{ Body:
|
|
18
|
+
request: FastifyRequest<{ Body: BlockIpInput }>,
|
|
25
19
|
reply: FastifyReply,
|
|
26
20
|
): Promise<void> {
|
|
27
21
|
const parsed = blockIpSchema.safeParse(request.body);
|
|
@@ -2,6 +2,7 @@ import type { FastifyInstance } from 'fastify';
|
|
|
2
2
|
import { authenticate, authorize } from '@libs/auth.js';
|
|
3
3
|
import { RATE_LIMITS } from '@config/rate-limit.config.js';
|
|
4
4
|
import * as adminController from './admin.controller.js';
|
|
5
|
+
import { blockIpSchema, unblockIpParamsSchema } from './admin.schemas.js';
|
|
5
6
|
|
|
6
7
|
export async function adminRoutes(fastify: FastifyInstance): Promise<void> {
|
|
7
8
|
// All admin routes require authentication + ADMIN role
|
|
@@ -28,6 +29,9 @@ export async function adminRoutes(fastify: FastifyInstance): Promise<void> {
|
|
|
28
29
|
* Body: { ip: string }
|
|
29
30
|
*/
|
|
30
31
|
fastify.post('/admin/blocked-ips', {
|
|
32
|
+
schema: {
|
|
33
|
+
body: blockIpSchema,
|
|
34
|
+
},
|
|
31
35
|
config: { rateLimit: RATE_LIMITS.ADMIN_DEFAULT },
|
|
32
36
|
handler: adminController.blockIpHandler,
|
|
33
37
|
});
|
|
@@ -39,6 +43,9 @@ export async function adminRoutes(fastify: FastifyInstance): Promise<void> {
|
|
|
39
43
|
* Auth: Required (ADMIN)
|
|
40
44
|
*/
|
|
41
45
|
fastify.delete('/admin/blocked-ips/:ip', {
|
|
46
|
+
schema: {
|
|
47
|
+
params: unblockIpParamsSchema,
|
|
48
|
+
},
|
|
42
49
|
config: { rateLimit: RATE_LIMITS.ADMIN_DEFAULT },
|
|
43
50
|
handler: adminController.unblockIpHandler,
|
|
44
51
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const blockIpSchema = z.object({
|
|
4
|
+
ip: z.union([z.ipv4(), z.ipv6()], { message: 'Invalid IP address' }),
|
|
5
|
+
reason: z.string().max(500).optional(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const unblockIpParamsSchema = z.object({
|
|
9
|
+
ip: z.string().min(1, 'IP address is required'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type BlockIpInput = z.infer<typeof blockIpSchema>;
|
|
13
|
+
export type UnblockIpParams = z.infer<typeof unblockIpParamsSchema>;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { prisma } from '@libs/prisma.js';
|
|
2
|
+
import type { User, RefreshToken } from '@prisma/client';
|
|
2
3
|
|
|
3
|
-
export async function findUserByEmail(email: string) {
|
|
4
|
+
export async function findUserByEmail(email: string): Promise<User | null> {
|
|
4
5
|
return prisma.user.findUnique({
|
|
5
6
|
where: { email, deletedAt: null },
|
|
6
7
|
});
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export async function findDeletedUserByEmail(email: string) {
|
|
10
|
+
export async function findDeletedUserByEmail(email: string): Promise<User | null> {
|
|
10
11
|
return prisma.user.findFirst({
|
|
11
12
|
where: { email, deletedAt: { not: null } },
|
|
12
13
|
});
|
|
@@ -24,7 +25,7 @@ export async function restoreUser(userId: string): Promise<void> {
|
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export async function findUserById(id: string) {
|
|
28
|
+
export async function findUserById(id: string): Promise<User | null> {
|
|
28
29
|
return prisma.user.findUnique({
|
|
29
30
|
where: { id, deletedAt: null },
|
|
30
31
|
});
|
|
@@ -35,7 +36,7 @@ export async function createUser(data: {
|
|
|
35
36
|
password: string;
|
|
36
37
|
firstName: string;
|
|
37
38
|
lastName: string;
|
|
38
|
-
}) {
|
|
39
|
+
}): Promise<User> {
|
|
39
40
|
return prisma.user.create({
|
|
40
41
|
data,
|
|
41
42
|
});
|
|
@@ -45,13 +46,13 @@ export async function createRefreshToken(data: {
|
|
|
45
46
|
token: string;
|
|
46
47
|
userId: string;
|
|
47
48
|
expiresAt: Date;
|
|
48
|
-
}) {
|
|
49
|
+
}): Promise<RefreshToken> {
|
|
49
50
|
return prisma.refreshToken.create({
|
|
50
51
|
data,
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
export async function findRefreshToken(token: string) {
|
|
55
|
+
export async function findRefreshToken(token: string): Promise<RefreshToken | null> {
|
|
55
56
|
return prisma.refreshToken.findUnique({
|
|
56
57
|
where: { token },
|
|
57
58
|
});
|
|
@@ -98,8 +99,8 @@ export async function rotateRefreshToken(
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
export async function deleteRefreshTokensByUserId(userId: string) {
|
|
102
|
-
|
|
102
|
+
export async function deleteRefreshTokensByUserId(userId: string): Promise<void> {
|
|
103
|
+
await prisma.refreshToken.deleteMany({
|
|
103
104
|
where: { userId },
|
|
104
105
|
});
|
|
105
106
|
}
|