create-nextjs-stack 0.1.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/LICENSE +21 -0
- package/README.md +60 -0
- package/bin/cli.js +187 -0
- package/package.json +48 -0
- package/templates/admin/.env.example +11 -0
- package/templates/admin/README.md +82 -0
- package/templates/admin/app/(auth)/login/page.tsx +84 -0
- package/templates/admin/app/(dashboard)/[resource]/[id]/page.tsx +45 -0
- package/templates/admin/app/(dashboard)/[resource]/new/page.tsx +32 -0
- package/templates/admin/app/(dashboard)/[resource]/page.tsx +131 -0
- package/templates/admin/app/(dashboard)/categories/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/categories/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/categories/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/clients/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/clients/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/clients/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/dashboard/page.tsx +45 -0
- package/templates/admin/app/(dashboard)/layout.tsx +13 -0
- package/templates/admin/app/(dashboard)/products/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/products/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/products/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/projects/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/projects/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/projects/page.tsx +33 -0
- package/templates/admin/app/(dashboard)/users/[id]/page.tsx +22 -0
- package/templates/admin/app/(dashboard)/users/new/page.tsx +5 -0
- package/templates/admin/app/(dashboard)/users/page.tsx +33 -0
- package/templates/admin/app/actions/resources.ts +46 -0
- package/templates/admin/app/actions/upload.ts +58 -0
- package/templates/admin/app/favicon.ico +0 -0
- package/templates/admin/app/globals.css +23 -0
- package/templates/admin/app/layout.tsx +23 -0
- package/templates/admin/app/page.tsx +5 -0
- package/templates/admin/components/admin/AdminLayoutClient.tsx +22 -0
- package/templates/admin/components/admin/DeleteModal.tsx +90 -0
- package/templates/admin/components/admin/FormLayout.tsx +113 -0
- package/templates/admin/components/admin/ImageUpload.tsx +137 -0
- package/templates/admin/components/admin/ResourceFormClient.tsx +62 -0
- package/templates/admin/components/admin/Sidebar.tsx +74 -0
- package/templates/admin/components/admin/SubmitButton.tsx +34 -0
- package/templates/admin/components/admin/ToastProvider.tsx +8 -0
- package/templates/admin/components/categories/CategoryForm.tsx +24 -0
- package/templates/admin/components/categories/CategoryList.tsx +113 -0
- package/templates/admin/components/clients/ClientForm.tsx +24 -0
- package/templates/admin/components/clients/ClientList.tsx +113 -0
- package/templates/admin/components/products/ProductForm.tsx +24 -0
- package/templates/admin/components/products/ProductList.tsx +117 -0
- package/templates/admin/components/projects/ProjectForm.tsx +24 -0
- package/templates/admin/components/projects/ProjectList.tsx +121 -0
- package/templates/admin/components/users/UserForm.tsx +39 -0
- package/templates/admin/components/users/UserList.tsx +101 -0
- package/templates/admin/config/resources.ts +123 -0
- package/templates/admin/eslint.config.mjs +18 -0
- package/templates/admin/hooks/useResource.ts +86 -0
- package/templates/admin/lib/services/base.service.ts +106 -0
- package/templates/admin/lib/services/categories.service.ts +7 -0
- package/templates/admin/lib/services/clients.service.ts +7 -0
- package/templates/admin/lib/services/index.ts +27 -0
- package/templates/admin/lib/services/products.service.ts +9 -0
- package/templates/admin/lib/services/projects.service.ts +22 -0
- package/templates/admin/lib/services/resource.service.ts +26 -0
- package/templates/admin/lib/services/users.service.ts +9 -0
- package/templates/admin/lib/supabase/client.ts +9 -0
- package/templates/admin/lib/supabase/middleware.ts +57 -0
- package/templates/admin/lib/supabase/server.ts +29 -0
- package/templates/admin/middleware.ts +15 -0
- package/templates/admin/next.config.ts +10 -0
- package/templates/admin/package-lock.json +6768 -0
- package/templates/admin/package.json +33 -0
- package/templates/admin/postcss.config.mjs +7 -0
- package/templates/admin/public/file.svg +1 -0
- package/templates/admin/public/globe.svg +1 -0
- package/templates/admin/public/next.svg +1 -0
- package/templates/admin/public/vercel.svg +1 -0
- package/templates/admin/public/window.svg +1 -0
- package/templates/admin/supabase_mock_data.sql +57 -0
- package/templates/admin/supabase_schema.sql +93 -0
- package/templates/admin/tsconfig.json +34 -0
- package/templates/web/.env.example +21 -0
- package/templates/web/README.md +129 -0
- package/templates/web/components.json +22 -0
- package/templates/web/eslint.config.mjs +25 -0
- package/templates/web/next.config.ts +25 -0
- package/templates/web/package-lock.json +6778 -0
- package/templates/web/package.json +45 -0
- package/templates/web/postcss.config.mjs +5 -0
- package/templates/web/src/app/api/contact/route.ts +181 -0
- package/templates/web/src/app/api/revalidate/route.ts +95 -0
- package/templates/web/src/app/error.tsx +28 -0
- package/templates/web/src/app/globals.css +838 -0
- package/templates/web/src/app/layout.tsx +126 -0
- package/templates/web/src/app/loading.tsx +60 -0
- package/templates/web/src/app/not-found.tsx +68 -0
- package/templates/web/src/app/page.tsx +106 -0
- package/templates/web/src/app/robots.ts +12 -0
- package/templates/web/src/app/sitemap.ts +66 -0
- package/templates/web/src/components/home/StatsGrid.tsx +89 -0
- package/templates/web/src/hooks/useIntersectionObserver.ts +39 -0
- package/templates/web/src/lib/providers/StoreProvider.tsx +12 -0
- package/templates/web/src/lib/seo/index.ts +4 -0
- package/templates/web/src/lib/seo/metadata.ts +103 -0
- package/templates/web/src/lib/seo/seo.config.ts +161 -0
- package/templates/web/src/lib/seo/seo.types.ts +76 -0
- package/templates/web/src/lib/services/categories.service.ts +38 -0
- package/templates/web/src/lib/services/categoryService.ts +251 -0
- package/templates/web/src/lib/services/clientService.ts +132 -0
- package/templates/web/src/lib/services/clients.service.ts +20 -0
- package/templates/web/src/lib/services/productService.ts +261 -0
- package/templates/web/src/lib/services/products.service.ts +38 -0
- package/templates/web/src/lib/services/projectService.ts +234 -0
- package/templates/web/src/lib/services/projects.service.ts +38 -0
- package/templates/web/src/lib/services/users.service.ts +20 -0
- package/templates/web/src/lib/supabase/client.ts +42 -0
- package/templates/web/src/lib/supabase/constants.ts +25 -0
- package/templates/web/src/lib/supabase/server.ts +29 -0
- package/templates/web/src/lib/supabase/types.ts +112 -0
- package/templates/web/src/lib/utils/cache.ts +98 -0
- package/templates/web/src/lib/utils/rate-limiter.ts +102 -0
- package/templates/web/src/store/actions/index.ts +2 -0
- package/templates/web/src/store/index.ts +13 -0
- package/templates/web/src/store/reducers/index.ts +13 -0
- package/templates/web/src/store/types/index.ts +2 -0
- package/templates/web/tsconfig.json +41 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getServerClient } from "@/lib/supabase/server";
|
|
2
|
+
|
|
3
|
+
export class ProjectService {
|
|
4
|
+
private static table = "projects";
|
|
5
|
+
|
|
6
|
+
static async getAll() {
|
|
7
|
+
const supabase = await getServerClient();
|
|
8
|
+
const { data, error } = await supabase
|
|
9
|
+
.from(this.table)
|
|
10
|
+
.select("*, clients(name, logo_url)")
|
|
11
|
+
.eq("published", true)
|
|
12
|
+
.order("created_at", { ascending: false });
|
|
13
|
+
|
|
14
|
+
if (error) {
|
|
15
|
+
console.error(`Error fetching ${this.table}:`, error);
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static async getBySlug(slug: string) {
|
|
23
|
+
const supabase = await getServerClient();
|
|
24
|
+
const { data, error } = await supabase
|
|
25
|
+
.from(this.table)
|
|
26
|
+
.select("*, clients(name, logo_url)")
|
|
27
|
+
.eq("slug", slug)
|
|
28
|
+
.eq("published", true)
|
|
29
|
+
.single();
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
console.error(`Error fetching ${this.table} ${slug}:`, error);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getServerClient } from "@/lib/supabase/server";
|
|
2
|
+
|
|
3
|
+
export class UserService {
|
|
4
|
+
private static table = "users";
|
|
5
|
+
|
|
6
|
+
static async getAll() {
|
|
7
|
+
const supabase = await getServerClient();
|
|
8
|
+
const { data, error } = await supabase
|
|
9
|
+
.from(this.table)
|
|
10
|
+
.select("*")
|
|
11
|
+
.order("created_at", { ascending: false });
|
|
12
|
+
|
|
13
|
+
if (error) {
|
|
14
|
+
console.error(`Error fetching ${this.table}:`, error);
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import { createBrowserClient } from '@supabase/ssr';
|
|
3
|
+
import type { Database } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get Supabase client for server-side operations
|
|
7
|
+
* Uses SERVICE_ROLE_KEY for full database access
|
|
8
|
+
*/
|
|
9
|
+
export function getServerClient() {
|
|
10
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
11
|
+
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
12
|
+
|
|
13
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'Missing Supabase credentials. Please check NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in .env'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return createClient<Database>(supabaseUrl, supabaseKey, {
|
|
20
|
+
auth: {
|
|
21
|
+
persistSession: false,
|
|
22
|
+
autoRefreshToken: false,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get Supabase client for browser-side operations
|
|
29
|
+
* Uses createBrowserClient from @supabase/ssr to properly handle Cookies
|
|
30
|
+
*/
|
|
31
|
+
export function getBrowserClient() {
|
|
32
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
33
|
+
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
34
|
+
|
|
35
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'Missing Supabase credentials. Please check NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in .env'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return createBrowserClient<Database>(supabaseUrl, supabaseKey);
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const TABLE_NAMES = {
|
|
2
|
+
CATEGORIES: 'categories',
|
|
3
|
+
PRODUCTS: 'products',
|
|
4
|
+
CLIENTS: 'clients',
|
|
5
|
+
PROJECTS: 'projects',
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export const CACHE_TIMES = {
|
|
9
|
+
CATEGORIES: 3600, // 1 hour
|
|
10
|
+
PRODUCTS: 1800, // 30 minutes
|
|
11
|
+
CLIENTS: 3600, // 1 hour
|
|
12
|
+
PROJECTS: 1800, // 30 minutes
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const CACHE_TAGS = {
|
|
16
|
+
CATEGORIES: 'categories',
|
|
17
|
+
PRODUCTS: 'products',
|
|
18
|
+
CLIENTS: 'clients',
|
|
19
|
+
PROJECTS: 'projects',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export const RATE_LIMITS = {
|
|
23
|
+
REQUESTS_PER_MINUTE: 60,
|
|
24
|
+
REQUESTS_PER_HOUR: 1000,
|
|
25
|
+
} as const;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createServerClient } from "@supabase/ssr";
|
|
2
|
+
import { cookies } from "next/headers";
|
|
3
|
+
|
|
4
|
+
export async function getServerClient() {
|
|
5
|
+
const cookieStore = await cookies();
|
|
6
|
+
|
|
7
|
+
return createServerClient(
|
|
8
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
9
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
10
|
+
{
|
|
11
|
+
cookies: {
|
|
12
|
+
getAll() {
|
|
13
|
+
return cookieStore.getAll();
|
|
14
|
+
},
|
|
15
|
+
setAll(cookiesToSet) {
|
|
16
|
+
try {
|
|
17
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
18
|
+
cookieStore.set(name, value, options)
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
// The `setAll` method was called from a Server Component.
|
|
22
|
+
// This can be ignored if you have middleware refreshing
|
|
23
|
+
// user sessions.
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Database Row Types
|
|
2
|
+
export interface Category {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
description: string | null;
|
|
7
|
+
parent_id: string | null;
|
|
8
|
+
image_url: string | null;
|
|
9
|
+
published: boolean;
|
|
10
|
+
featured: boolean;
|
|
11
|
+
meta_description: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
updated_at: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Product {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
short_description: string | null;
|
|
21
|
+
description: string | null;
|
|
22
|
+
category_id: string;
|
|
23
|
+
featured_image_url: string | null;
|
|
24
|
+
gallery_images: string[];
|
|
25
|
+
width: number | null;
|
|
26
|
+
height: number | null;
|
|
27
|
+
thickness: number | null;
|
|
28
|
+
material: string | null;
|
|
29
|
+
surface_finish: string | null;
|
|
30
|
+
fire_rating: string | null;
|
|
31
|
+
weight_per_sqm: number | null;
|
|
32
|
+
available_colors: string | null;
|
|
33
|
+
installation_method: string | null;
|
|
34
|
+
certifications: string | null;
|
|
35
|
+
additional_features: string | null;
|
|
36
|
+
meta_description: string | null;
|
|
37
|
+
published: boolean;
|
|
38
|
+
featured: boolean;
|
|
39
|
+
created_at: string;
|
|
40
|
+
updated_at: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Client {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
website: string | null;
|
|
47
|
+
logo_url: string | null;
|
|
48
|
+
sort_order: number;
|
|
49
|
+
published: boolean;
|
|
50
|
+
created_at: string;
|
|
51
|
+
updated_at: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Project {
|
|
55
|
+
id: string;
|
|
56
|
+
title: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
short_description: string | null;
|
|
59
|
+
description: string | null;
|
|
60
|
+
location: string | null;
|
|
61
|
+
year: number;
|
|
62
|
+
client_name: string | null;
|
|
63
|
+
featured_image_url: string | null;
|
|
64
|
+
gallery_images: string[];
|
|
65
|
+
product_ids: string[];
|
|
66
|
+
meta_description: string | null;
|
|
67
|
+
published: boolean;
|
|
68
|
+
featured: boolean;
|
|
69
|
+
created_at: string;
|
|
70
|
+
updated_at: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Extended Types with Relations
|
|
74
|
+
export interface CategoryWithChildren extends Category {
|
|
75
|
+
children?: CategoryWithChildren[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ProductWithCategory extends Product {
|
|
79
|
+
category: Category | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ProjectWithProducts extends Project {
|
|
83
|
+
products: Product[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Database Type (for Supabase client)
|
|
87
|
+
export interface Database {
|
|
88
|
+
public: {
|
|
89
|
+
Tables: {
|
|
90
|
+
categories: {
|
|
91
|
+
Row: Category;
|
|
92
|
+
Insert: Omit<Category, 'id' | 'created_at' | 'updated_at'>;
|
|
93
|
+
Update: Partial<Omit<Category, 'id' | 'created_at' | 'updated_at'>>;
|
|
94
|
+
};
|
|
95
|
+
products: {
|
|
96
|
+
Row: Product;
|
|
97
|
+
Insert: Omit<Product, 'id' | 'created_at' | 'updated_at'>;
|
|
98
|
+
Update: Partial<Omit<Product, 'id' | 'created_at' | 'updated_at'>>;
|
|
99
|
+
};
|
|
100
|
+
clients: {
|
|
101
|
+
Row: Client;
|
|
102
|
+
Insert: Omit<Client, 'id' | 'created_at' | 'updated_at'>;
|
|
103
|
+
Update: Partial<Omit<Client, 'id' | 'created_at' | 'updated_at'>>;
|
|
104
|
+
};
|
|
105
|
+
projects: {
|
|
106
|
+
Row: Project;
|
|
107
|
+
Insert: Omit<Project, 'id' | 'created_at' | 'updated_at'>;
|
|
108
|
+
Update: Partial<Omit<Project, 'id' | 'created_at' | 'updated_at'>>;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
interface CacheEntry<T> {
|
|
2
|
+
data: T;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
ttl: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* In-memory cache with TTL (Time To Live) support
|
|
9
|
+
* Provides fast access to frequently requested data
|
|
10
|
+
*/
|
|
11
|
+
class MemoryCache {
|
|
12
|
+
private cache: Map<string, CacheEntry<unknown>> = new Map();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Store data in cache with TTL
|
|
16
|
+
* @param key - Cache key
|
|
17
|
+
* @param data - Data to cache
|
|
18
|
+
* @param ttl - Time to live in seconds
|
|
19
|
+
*/
|
|
20
|
+
set<T>(key: string, data: T, ttl: number): void {
|
|
21
|
+
this.cache.set(key, {
|
|
22
|
+
data,
|
|
23
|
+
timestamp: Date.now(),
|
|
24
|
+
ttl,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Retrieve data from cache
|
|
30
|
+
* @param key - Cache key
|
|
31
|
+
* @returns Cached data or null if expired/not found
|
|
32
|
+
*/
|
|
33
|
+
get<T>(key: string): T | null {
|
|
34
|
+
const entry = this.cache.get(key) as CacheEntry<T> | undefined;
|
|
35
|
+
|
|
36
|
+
if (!entry) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if entry has expired
|
|
41
|
+
const age = Date.now() - entry.timestamp;
|
|
42
|
+
if (age > entry.ttl * 1000) {
|
|
43
|
+
this.cache.delete(key);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return entry.data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if key exists and is not expired
|
|
52
|
+
* @param key - Cache key
|
|
53
|
+
*/
|
|
54
|
+
has(key: string): boolean {
|
|
55
|
+
return this.get(key) !== null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Invalidate cache entries matching a pattern
|
|
60
|
+
* @param pattern - String pattern to match against keys
|
|
61
|
+
*/
|
|
62
|
+
invalidate(pattern: string): void {
|
|
63
|
+
const keys = Array.from(this.cache.keys());
|
|
64
|
+
keys.forEach((key) => {
|
|
65
|
+
if (key.includes(pattern)) {
|
|
66
|
+
this.cache.delete(key);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Invalidate specific cache key
|
|
73
|
+
* @param key - Cache key to invalidate
|
|
74
|
+
*/
|
|
75
|
+
invalidateKey(key: string): void {
|
|
76
|
+
this.cache.delete(key);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Clear all cache entries
|
|
81
|
+
*/
|
|
82
|
+
clear(): void {
|
|
83
|
+
this.cache.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get cache statistics
|
|
88
|
+
*/
|
|
89
|
+
getStats() {
|
|
90
|
+
return {
|
|
91
|
+
size: this.cache.size,
|
|
92
|
+
keys: Array.from(this.cache.keys()),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Export singleton instance
|
|
98
|
+
export const memoryCache = new MemoryCache();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { RATE_LIMITS } from '@/lib/supabase/constants';
|
|
2
|
+
|
|
3
|
+
interface RateLimitEntry {
|
|
4
|
+
tokens: number;
|
|
5
|
+
lastRefill: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Token Bucket Rate Limiter
|
|
10
|
+
* Implements the token bucket algorithm for rate limiting
|
|
11
|
+
*/
|
|
12
|
+
class RateLimiter {
|
|
13
|
+
private buckets: Map<string, RateLimitEntry> = new Map();
|
|
14
|
+
private maxTokens: number;
|
|
15
|
+
private refillRate: number; // tokens per second
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param maxTokens - Maximum number of tokens in the bucket
|
|
19
|
+
* @param refillRate - Number of tokens added per second
|
|
20
|
+
*/
|
|
21
|
+
constructor(maxTokens: number, refillRate: number) {
|
|
22
|
+
this.maxTokens = maxTokens;
|
|
23
|
+
this.refillRate = refillRate;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if request is allowed under rate limit
|
|
28
|
+
* @param key - Unique identifier for the rate limit bucket (e.g., user ID, IP, endpoint)
|
|
29
|
+
* @returns true if request is allowed, false if rate limited
|
|
30
|
+
*/
|
|
31
|
+
async checkLimit(key: string): Promise<boolean> {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
let bucket = this.buckets.get(key);
|
|
34
|
+
|
|
35
|
+
// Initialize bucket if it doesn't exist
|
|
36
|
+
if (!bucket) {
|
|
37
|
+
bucket = {
|
|
38
|
+
tokens: this.maxTokens - 1,
|
|
39
|
+
lastRefill: now,
|
|
40
|
+
};
|
|
41
|
+
this.buckets.set(key, bucket);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Refill tokens based on time passed
|
|
46
|
+
const timePassed = (now - bucket.lastRefill) / 1000; // in seconds
|
|
47
|
+
const tokensToAdd = timePassed * this.refillRate;
|
|
48
|
+
bucket.tokens = Math.min(this.maxTokens, bucket.tokens + tokensToAdd);
|
|
49
|
+
bucket.lastRefill = now;
|
|
50
|
+
|
|
51
|
+
// Check if we have tokens available
|
|
52
|
+
if (bucket.tokens >= 1) {
|
|
53
|
+
bucket.tokens -= 1;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Rate limit exceeded
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get remaining tokens for a key
|
|
63
|
+
* @param key - Rate limit bucket key
|
|
64
|
+
*/
|
|
65
|
+
getRemainingTokens(key: string): number {
|
|
66
|
+
const bucket = this.buckets.get(key);
|
|
67
|
+
if (!bucket) {
|
|
68
|
+
return this.maxTokens;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const timePassed = (now - bucket.lastRefill) / 1000;
|
|
73
|
+
const tokensToAdd = timePassed * this.refillRate;
|
|
74
|
+
return Math.min(this.maxTokens, bucket.tokens + tokensToAdd);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reset rate limit for a specific key
|
|
79
|
+
* @param key - Rate limit bucket key
|
|
80
|
+
*/
|
|
81
|
+
reset(key: string): void {
|
|
82
|
+
this.buckets.delete(key);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Clear all rate limit buckets
|
|
87
|
+
*/
|
|
88
|
+
clearAll(): void {
|
|
89
|
+
this.buckets.clear();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Global rate limiters
|
|
94
|
+
export const perMinuteLimiter = new RateLimiter(
|
|
95
|
+
RATE_LIMITS.REQUESTS_PER_MINUTE,
|
|
96
|
+
RATE_LIMITS.REQUESTS_PER_MINUTE / 60 // tokens per second
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
export const perHourLimiter = new RateLimiter(
|
|
100
|
+
RATE_LIMITS.REQUESTS_PER_HOUR,
|
|
101
|
+
RATE_LIMITS.REQUESTS_PER_HOUR / 3600 // tokens per second
|
|
102
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createStore, applyMiddleware } from 'redux';
|
|
2
|
+
import { thunk } from 'redux-thunk';
|
|
3
|
+
import logger from 'redux-logger';
|
|
4
|
+
import rootReducer from './reducers';
|
|
5
|
+
|
|
6
|
+
const middleware = [thunk, logger];
|
|
7
|
+
|
|
8
|
+
const store = createStore(
|
|
9
|
+
rootReducer,
|
|
10
|
+
applyMiddleware(...middleware)
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export default store;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { combineReducers } from 'redux';
|
|
2
|
+
// Import your reducers here
|
|
3
|
+
// import exampleReducer from './exampleReducer';
|
|
4
|
+
|
|
5
|
+
const rootReducer = combineReducers({
|
|
6
|
+
// example: exampleReducer,
|
|
7
|
+
// Add a placeholder to prevent error if no reducers yet
|
|
8
|
+
_placeholder: (state = {}) => state
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type RootState = ReturnType<typeof rootReducer>;
|
|
12
|
+
|
|
13
|
+
export default rootReducer;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"esnext"
|
|
8
|
+
],
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"module": "esnext",
|
|
15
|
+
"moduleResolution": "bundler",
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
"incremental": true,
|
|
20
|
+
"plugins": [
|
|
21
|
+
{
|
|
22
|
+
"name": "next"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"paths": {
|
|
26
|
+
"@/*": [
|
|
27
|
+
"./src/*"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"include": [
|
|
32
|
+
"next-env.d.ts",
|
|
33
|
+
"**/*.ts",
|
|
34
|
+
"**/*.tsx",
|
|
35
|
+
".next/types/**/*.ts",
|
|
36
|
+
".next/dev/types/**/*.ts"
|
|
37
|
+
],
|
|
38
|
+
"exclude": [
|
|
39
|
+
"node_modules"
|
|
40
|
+
]
|
|
41
|
+
}
|