@ahmadubaidillah/cli 1.1.2 → 1.1.4
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/bin.js +59 -17
- package/dist/plugins/admin-panel/files/src/modules/admin/components/OverviewChart.tsx +56 -0
- package/dist/plugins/admin-panel/files/src/modules/admin/services/admin.service.ts +40 -0
- package/dist/plugins/admin-panel/files/src/modules/admin/views/AdminDashboard.tsx +106 -0
- package/dist/plugins/admin-panel/plugin.config.json +10 -0
- package/dist/plugins/admin-panel/tsconfig.json +12 -0
- package/dist/plugins/analytics/files/src/modules/analytics/services/analytics.service.ts +71 -0
- package/dist/plugins/analytics/plugin.config.json +8 -0
- package/dist/plugins/auth/files/src/modules/auth/auth.schema.ts +21 -0
- package/dist/plugins/auth/files/src/modules/auth/db/rls.ts +31 -0
- package/dist/plugins/auth/files/src/modules/auth/db/schema.ts +103 -0
- package/dist/plugins/auth/files/src/modules/auth/middleware/auth.middleware.ts +12 -0
- package/dist/plugins/auth/files/src/modules/auth/middleware/tenant.middleware.ts +50 -0
- package/dist/plugins/auth/files/src/modules/auth/routes/auth.routes.ts +40 -0
- package/dist/plugins/auth/files/src/modules/auth/services/auth.service.ts +113 -0
- package/dist/plugins/auth/plugin.config.json +9 -0
- package/dist/plugins/cms/files/src/modules/cms/cms.schema.ts +24 -0
- package/dist/plugins/cms/files/src/modules/cms/db/schema.ts +88 -0
- package/dist/plugins/cms/files/src/modules/cms/routes/cms.routes.ts +67 -0
- package/dist/plugins/cms/files/src/modules/cms/services/cms.service.ts +99 -0
- package/dist/plugins/cms/plugin.config.json +9 -0
- package/dist/plugins/deployment/files/Dockerfile +33 -0
- package/dist/plugins/deployment/files/docker-compose.yml +27 -0
- package/dist/plugins/deployment/files/vercel.json +14 -0
- package/dist/plugins/deployment/plugin.config.json +5 -0
- package/dist/plugins/email/files/src/modules/email/services/email.service.ts +30 -0
- package/dist/plugins/email/plugin.config.json +9 -0
- package/dist/plugins/file_upload/files/src/modules/storage/services/storage.service.ts +39 -0
- package/dist/plugins/file_upload/plugin.config.json +10 -0
- package/dist/plugins/github-actions/files/.github/workflows/ci.yml +34 -0
- package/dist/plugins/github-actions/plugin.config.json +14 -0
- package/dist/plugins/openapi/files/src/modules/openapi/openapi.routes.ts +17 -0
- package/dist/plugins/openapi/files/src/modules/openapi/openapi.schema.ts +10 -0
- package/dist/plugins/openapi/plugin.config.json +10 -0
- package/dist/plugins/payments/files/src/modules/billing/billing.schema.ts +14 -0
- package/dist/plugins/payments/files/src/modules/billing/routes/billing.routes.ts +57 -0
- package/dist/plugins/payments/files/src/modules/billing/services/stripe.service.ts +47 -0
- package/dist/plugins/payments/plugin.config.json +10 -0
- package/dist/plugins/queue/files/src/modules/queue/services/queue.service.ts +61 -0
- package/dist/plugins/queue/plugin.config.json +10 -0
- package/dist/plugins/search/files/src/modules/search/services/search.service.ts +98 -0
- package/dist/plugins/search/plugin.config.json +9 -0
- package/dist/plugins/websocket/files/src/modules/websocket/services/ws.service.ts +51 -0
- package/dist/plugins/websocket/plugin.config.json +7 -0
- package/dist/templates/templates/saas/files/package.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { signupSchema, loginSchema, organizationSchema } from '../auth.schema';
|
|
3
|
+
|
|
4
|
+
export const authRoutes = new Hono();
|
|
5
|
+
|
|
6
|
+
authRoutes.post('/signup', async (c) => {
|
|
7
|
+
const body = await c.req.json();
|
|
8
|
+
const result = signupSchema.safeParse(body);
|
|
9
|
+
if (!result.success) return c.json({ error: result.error.format() }, 400);
|
|
10
|
+
return c.json({ message: 'User signed up successfully' });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
authRoutes.post('/login', async (c) => {
|
|
14
|
+
const body = await c.req.json();
|
|
15
|
+
const result = loginSchema.safeParse(body);
|
|
16
|
+
if (!result.success) return c.json({ error: result.error.format() }, 400);
|
|
17
|
+
return c.json({ message: 'User logged in successfully' });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
authRoutes.get('/me', (c) => {
|
|
21
|
+
return c.json({ user: { id: '1', email: 'user@example.com' } });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Organization Management
|
|
26
|
+
*/
|
|
27
|
+
authRoutes.post('/organizations', async (c) => {
|
|
28
|
+
const body = await c.req.json();
|
|
29
|
+
const result = organizationSchema.safeParse(body);
|
|
30
|
+
if (!result.success) return c.json({ error: result.error.format() }, 400);
|
|
31
|
+
return c.json({ message: 'Organization created successfully', id: 'org_123' });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
authRoutes.get('/organizations', (c) => {
|
|
35
|
+
return c.json({
|
|
36
|
+
organizations: [
|
|
37
|
+
{ id: 'org_123', name: 'DevForge Team', slug: 'devforge' }
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { betterAuth } from "better-auth";
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { organization } from "better-auth/plugins";
|
|
4
|
+
// In a scaffolded project, this usually resolves to @devforge/core or relative path
|
|
5
|
+
// For the purpose of the SDK, we'll assume a relative path for now
|
|
6
|
+
import { eventBus } from "../../../../../../../core/src/hooks/event-bus";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* AuthService
|
|
10
|
+
*
|
|
11
|
+
* This service provides a robust wrapper around Better Auth.
|
|
12
|
+
* It is designed to work with Drizzle ORM.
|
|
13
|
+
*/
|
|
14
|
+
export class AuthService {
|
|
15
|
+
public auth: any;
|
|
16
|
+
|
|
17
|
+
constructor(db: any, provider: "pg" | "sqlite" | "mysql" = "pg", schema?: any) {
|
|
18
|
+
this.auth = betterAuth({
|
|
19
|
+
database: drizzleAdapter(db, {
|
|
20
|
+
provider,
|
|
21
|
+
schema,
|
|
22
|
+
}),
|
|
23
|
+
secret: process.env.BETTER_AUTH_SECRET || "development_secret_only_for_scaffolding",
|
|
24
|
+
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
|
25
|
+
emailAndPassword: {
|
|
26
|
+
enabled: true
|
|
27
|
+
},
|
|
28
|
+
plugins: [
|
|
29
|
+
organization({
|
|
30
|
+
allowUserToCreateOrganization: true,
|
|
31
|
+
creatorRole: "owner",
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Authenticate a user with email and password.
|
|
39
|
+
* Returns user data and headers to be sent to the client.
|
|
40
|
+
*/
|
|
41
|
+
async login(credentials: { email: string; password: string }) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await this.auth.api.signInEmail({
|
|
44
|
+
body: {
|
|
45
|
+
email: credentials.email,
|
|
46
|
+
password: credentials.password,
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// Better Auth returns a response object that can include headers for setting cookies
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
user: response.user,
|
|
53
|
+
session: response.session,
|
|
54
|
+
headers: response.headers
|
|
55
|
+
};
|
|
56
|
+
} catch (error: any) {
|
|
57
|
+
throw new Error(error.message || 'Login failed');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a new user account.
|
|
63
|
+
*/
|
|
64
|
+
async signup(data: any) {
|
|
65
|
+
try {
|
|
66
|
+
const response = await this.auth.api.signUpEmail({
|
|
67
|
+
body: {
|
|
68
|
+
email: data.email,
|
|
69
|
+
password: data.password,
|
|
70
|
+
name: data.name,
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
user: response.user,
|
|
76
|
+
session: response.session,
|
|
77
|
+
headers: response.headers
|
|
78
|
+
};
|
|
79
|
+
} catch (error: any) {
|
|
80
|
+
throw new Error(error.message || 'Signup failed');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create an organization.
|
|
86
|
+
*/
|
|
87
|
+
async createOrganization(data: any, user: any) {
|
|
88
|
+
try {
|
|
89
|
+
const org = await this.auth.api.createOrganization({
|
|
90
|
+
body: data,
|
|
91
|
+
headers: new Headers({ "authorization": `Bearer ${user.id}` }) // Mocking auth for example
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
eventBus.dispatch('org.created', { org, creatorId: user.id });
|
|
95
|
+
|
|
96
|
+
return org;
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
throw new Error(error.message || 'Organization creation failed');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Retrieve the current session from the request headers.
|
|
104
|
+
*/
|
|
105
|
+
async getSession(request: Request | { headers: Headers }) {
|
|
106
|
+
try {
|
|
107
|
+
return await this.auth.api.getSession({ headers: request.headers });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth",
|
|
3
|
+
"description": "Authentication module using Better Auth",
|
|
4
|
+
"compatibleTemplates": ["saas", "cms", "marketplace", "booking", "finance", "crm"],
|
|
5
|
+
"packageDependencies": {
|
|
6
|
+
"better-auth": "latest",
|
|
7
|
+
"@better-auth/drizzle-adapter": "latest"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const postSchema = z.object({
|
|
4
|
+
title: z.string().min(1),
|
|
5
|
+
content: z.string().min(1),
|
|
6
|
+
slug: z.string().min(1),
|
|
7
|
+
categoryId: z.string().optional(),
|
|
8
|
+
tagIds: z.array(z.string()).optional(),
|
|
9
|
+
status: z.enum(['draft', 'published']).default('draft'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const categorySchema = z.object({
|
|
13
|
+
name: z.string().min(1),
|
|
14
|
+
slug: z.string().min(1),
|
|
15
|
+
description: z.string().optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const tagSchema = z.object({
|
|
19
|
+
name: z.string().min(1),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type PostInput = z.infer<typeof postSchema>;
|
|
23
|
+
export type CategoryInput = z.infer<typeof categorySchema>;
|
|
24
|
+
export type TagInput = z.infer<typeof tagSchema>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, uuid, index, primaryKey, jsonb } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Categories Table
|
|
5
|
+
* Allows organizing posts into primary buckets.
|
|
6
|
+
*/
|
|
7
|
+
export const categories = pgTable("category", {
|
|
8
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
9
|
+
name: text("name").notNull(),
|
|
10
|
+
slug: text("slug").notNull(),
|
|
11
|
+
organizationId: text("organization_id").notNull(),
|
|
12
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
13
|
+
}, (table) => [
|
|
14
|
+
index("category_org_idx").on(table.organizationId),
|
|
15
|
+
index("category_slug_idx").on(table.slug),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tags Table
|
|
20
|
+
* Flexible labeling system for posts.
|
|
21
|
+
*/
|
|
22
|
+
export const tags = pgTable("tag", {
|
|
23
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
24
|
+
name: text("name").notNull(),
|
|
25
|
+
slug: text("slug").notNull(),
|
|
26
|
+
organizationId: text("organization_id").notNull(),
|
|
27
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
28
|
+
}, (table) => [
|
|
29
|
+
index("tag_org_idx").on(table.organizationId),
|
|
30
|
+
index("tag_slug_idx").on(table.slug),
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Posts Table
|
|
35
|
+
* Updated with category, featured image, and SEO metadata.
|
|
36
|
+
*/
|
|
37
|
+
export const posts = pgTable("post", {
|
|
38
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
39
|
+
title: text("title").notNull(),
|
|
40
|
+
slug: text("slug").notNull(),
|
|
41
|
+
content: jsonb("content"), // Support for rich text JSON
|
|
42
|
+
seo: jsonb("seo"), // Metadata: title, description, ogImage
|
|
43
|
+
status: text("status", { enum: ["draft", "published"] }).default("draft").notNull(),
|
|
44
|
+
authorId: text("author_id").notNull(),
|
|
45
|
+
organizationId: text("organization_id").notNull(),
|
|
46
|
+
categoryId: uuid("category_id").references(() => categories.id),
|
|
47
|
+
featuredImageId: text("featured_image_id"), // Refers to external storage plugin
|
|
48
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
49
|
+
updatedAt: timestamp("updated_at")
|
|
50
|
+
.defaultNow()
|
|
51
|
+
.$onUpdate(() => new Date())
|
|
52
|
+
.notNull(),
|
|
53
|
+
}, (table) => [
|
|
54
|
+
index("post_org_idx").on(table.organizationId),
|
|
55
|
+
index("post_slug_idx").on(table.slug),
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Post-Tags Junction Table
|
|
60
|
+
* Many-to-Many relationship between Posts and Tags.
|
|
61
|
+
*/
|
|
62
|
+
export const postTags = pgTable("post_tag", {
|
|
63
|
+
postId: uuid("post_id").notNull().references(() => posts.id, { onDelete: 'cascade' }),
|
|
64
|
+
tagId: uuid("tag_id").notNull().references(() => tags.id, { onDelete: 'cascade' }),
|
|
65
|
+
}, (table) => [
|
|
66
|
+
primaryKey({ columns: [table.postId, table.tagId] })
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Pages Table
|
|
71
|
+
* Updated with SEO metadata.
|
|
72
|
+
*/
|
|
73
|
+
export const pages = pgTable("page", {
|
|
74
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
75
|
+
title: text("title").notNull(),
|
|
76
|
+
slug: text("slug").notNull(),
|
|
77
|
+
content: jsonb("content"),
|
|
78
|
+
seo: jsonb("seo"),
|
|
79
|
+
organizationId: text("organization_id").notNull(),
|
|
80
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
81
|
+
updatedAt: timestamp("updated_at")
|
|
82
|
+
.defaultNow()
|
|
83
|
+
.$onUpdate(() => new Date())
|
|
84
|
+
.notNull(),
|
|
85
|
+
}, (table) => [
|
|
86
|
+
index("page_org_idx").on(table.organizationId),
|
|
87
|
+
index("page_slug_idx").on(table.slug),
|
|
88
|
+
]);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { CMSService } from '../services/cms.service';
|
|
3
|
+
import { postSchema, categorySchema, tagSchema } from '../cms.schema';
|
|
4
|
+
|
|
5
|
+
export const cmsRoutes = new Hono<{
|
|
6
|
+
Variables: {
|
|
7
|
+
organizationId: string;
|
|
8
|
+
db: any;
|
|
9
|
+
}
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
// --- Posts ---
|
|
13
|
+
cmsRoutes.get('/posts', async (c) => {
|
|
14
|
+
const cmsService = new CMSService(c.get('db'));
|
|
15
|
+
const categorySlug = c.req.query('category');
|
|
16
|
+
const posts = await cmsService.getPosts(c.get('organizationId'), { categorySlug });
|
|
17
|
+
return c.json({ posts });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
cmsRoutes.post('/posts', async (c) => {
|
|
21
|
+
const cmsService = new CMSService(c.get('db'));
|
|
22
|
+
const body = await c.req.json();
|
|
23
|
+
const result = postSchema.safeParse(body);
|
|
24
|
+
if (!result.success) return c.json({ error: result.error.format() }, 400);
|
|
25
|
+
const post = await cmsService.createPost(body, c.get('organizationId'));
|
|
26
|
+
return c.json({ post });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// --- Categories ---
|
|
30
|
+
cmsRoutes.get('/categories', async (c) => {
|
|
31
|
+
const cmsService = new CMSService(c.get('db'));
|
|
32
|
+
const categories = await cmsService.getCategories(c.get('organizationId'));
|
|
33
|
+
return c.json({ categories });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
cmsRoutes.post('/categories', async (c) => {
|
|
37
|
+
const cmsService = new CMSService(c.get('db'));
|
|
38
|
+
const body = await c.req.json();
|
|
39
|
+
const result = categorySchema.safeParse(body);
|
|
40
|
+
if (!result.success) return c.json({ error: result.error.format() }, 400);
|
|
41
|
+
const category = await cmsService.createCategory(body, c.get('organizationId'));
|
|
42
|
+
return c.json({ category });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// --- Tags ---
|
|
46
|
+
cmsRoutes.get('/tags', async (c) => {
|
|
47
|
+
const cmsService = new CMSService(c.get('db'));
|
|
48
|
+
const tags = await cmsService.getTags(c.get('organizationId'));
|
|
49
|
+
return c.json({ tags });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
cmsRoutes.post('/tags', async (c) => {
|
|
53
|
+
const cmsService = new CMSService(c.get('db'));
|
|
54
|
+
const body = await c.req.json();
|
|
55
|
+
const result = tagSchema.safeParse(body);
|
|
56
|
+
if (!result.success) return c.json({ error: result.error.format() }, 400);
|
|
57
|
+
const tag = await cmsService.createTag(body, c.get('organizationId'));
|
|
58
|
+
return c.json({ tag });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// --- Pages ---
|
|
62
|
+
cmsRoutes.get('/pages/:slug', async (c) => {
|
|
63
|
+
const cmsService = new CMSService(c.get('db'));
|
|
64
|
+
const page = await cmsService.getPageBySlug(c.req.param('slug'), c.get('organizationId'));
|
|
65
|
+
if (!page) return c.json({ message: 'Page not found' }, 404);
|
|
66
|
+
return c.json({ page });
|
|
67
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { eq, and, inArray } from 'drizzle-orm';
|
|
2
|
+
import { posts, pages, categories, tags, postTags } from '../db/schema';
|
|
3
|
+
import { eventBus } from '../../../../../../../core/src/hooks/event-bus';
|
|
4
|
+
|
|
5
|
+
export class CMSService {
|
|
6
|
+
private db: any;
|
|
7
|
+
|
|
8
|
+
constructor(db: any) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// --- Posts ---
|
|
13
|
+
async getPosts(organizationId: string, options: { categorySlug?: string } = {}) {
|
|
14
|
+
try {
|
|
15
|
+
let query = this.db.select().from(posts).where(eq(posts.organizationId, organizationId));
|
|
16
|
+
|
|
17
|
+
if (options.categorySlug) {
|
|
18
|
+
const cat = await this.getCategoryBySlug(options.categorySlug, organizationId);
|
|
19
|
+
if (cat) {
|
|
20
|
+
query = this.db.select().from(posts).where(
|
|
21
|
+
and(eq(posts.organizationId, organizationId), eq(posts.categoryId, cat.id))
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return await query;
|
|
27
|
+
} catch (error: any) {
|
|
28
|
+
console.error(`[CMSService] Failed to fetch posts: ${error.message}`);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async createPost(data: any, organizationId: string) {
|
|
34
|
+
try {
|
|
35
|
+
const { tagIds, ...postData } = data;
|
|
36
|
+
const [post] = await this.db.insert(posts).values({
|
|
37
|
+
...postData,
|
|
38
|
+
organizationId,
|
|
39
|
+
}).returning();
|
|
40
|
+
|
|
41
|
+
if (tagIds && tagIds.length > 0) {
|
|
42
|
+
await this.db.insert(postTags).values(
|
|
43
|
+
tagIds.map((tagId: string) => ({ postId: post.id, tagId }))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
eventBus.dispatch('cms.post.created', { post, organizationId });
|
|
48
|
+
|
|
49
|
+
return post;
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
console.error(`[CMSService] Post creation failed: ${error.message}`);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Categories ---
|
|
57
|
+
async getCategories(organizationId: string) {
|
|
58
|
+
return await this.db.select().from(categories).where(eq(categories.organizationId, organizationId));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async createCategory(data: any, organizationId: string) {
|
|
62
|
+
try {
|
|
63
|
+
return await this.db.insert(categories).values({
|
|
64
|
+
...data,
|
|
65
|
+
organizationId,
|
|
66
|
+
}).returning();
|
|
67
|
+
} catch (error: any) {
|
|
68
|
+
console.error(`[CMSService] Category creation failed: ${error.message}`);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getCategoryBySlug(slug: string, organizationId: string) {
|
|
74
|
+
const [category] = await this.db.select()
|
|
75
|
+
.from(categories)
|
|
76
|
+
.where(and(eq(categories.slug, slug), eq(categories.organizationId, organizationId)));
|
|
77
|
+
return category;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Tags ---
|
|
81
|
+
async getTags(organizationId: string) {
|
|
82
|
+
return await this.db.select().from(tags).where(eq(tags.organizationId, organizationId));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async createTag(data: any, organizationId: string) {
|
|
86
|
+
return await this.db.insert(tags).values({
|
|
87
|
+
...data,
|
|
88
|
+
organizationId,
|
|
89
|
+
}).returning();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Pages ---
|
|
93
|
+
async getPageBySlug(slug: string, organizationId: string) {
|
|
94
|
+
const [page] = await this.db.select()
|
|
95
|
+
.from(pages)
|
|
96
|
+
.where(and(eq(pages.slug, slug), eq(pages.organizationId, organizationId)));
|
|
97
|
+
return page;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Stage 1: Build
|
|
2
|
+
FROM oven/bun:latest AS builder
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
COPY package.json ./
|
|
7
|
+
COPY bun.lockb* ./
|
|
8
|
+
|
|
9
|
+
RUN bun install --frozen-lockfile
|
|
10
|
+
|
|
11
|
+
COPY . .
|
|
12
|
+
|
|
13
|
+
# If there's a build step, run it
|
|
14
|
+
RUN if [ -f "src/app.ts" ]; then bun build ./src/app.ts --outdir ./dist; fi
|
|
15
|
+
|
|
16
|
+
# Stage 2: Production
|
|
17
|
+
FROM oven/bun:distroless
|
|
18
|
+
|
|
19
|
+
WORKDIR /app
|
|
20
|
+
|
|
21
|
+
# Copy only the built assets and necessary files
|
|
22
|
+
COPY --from=builder /app/package.json ./
|
|
23
|
+
COPY --from=builder /app/src ./src
|
|
24
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
25
|
+
|
|
26
|
+
# Environment variables
|
|
27
|
+
ENV NODE_ENV=production
|
|
28
|
+
ENV PORT=3000
|
|
29
|
+
|
|
30
|
+
EXPOSE 3000
|
|
31
|
+
|
|
32
|
+
# Start the application
|
|
33
|
+
CMD ["run", "src/app.ts"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
"app-{{PROJECT_NAME}}":
|
|
5
|
+
container_name: "{{PROJECT_NAME}}-app"
|
|
6
|
+
build: .
|
|
7
|
+
ports:
|
|
8
|
+
- "3000:3000"
|
|
9
|
+
environment:
|
|
10
|
+
- NODE_ENV=development
|
|
11
|
+
- DATABASE_URL=postgres://user:pass@db:5432/devforge
|
|
12
|
+
depends_on:
|
|
13
|
+
- db
|
|
14
|
+
|
|
15
|
+
db:
|
|
16
|
+
image: postgres:15-alpine
|
|
17
|
+
environment:
|
|
18
|
+
- POSTGRES_USER=user
|
|
19
|
+
- POSTGRES_PASSWORD=pass
|
|
20
|
+
- POSTGRES_DB=devforge
|
|
21
|
+
volumes:
|
|
22
|
+
- pgdata:/var/lib/postgresql/data
|
|
23
|
+
ports:
|
|
24
|
+
- "5432:5432"
|
|
25
|
+
|
|
26
|
+
volumes:
|
|
27
|
+
pgdata:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Resend } from 'resend';
|
|
2
|
+
|
|
3
|
+
export interface EmailOptions {
|
|
4
|
+
to: string | string[];
|
|
5
|
+
subject: string;
|
|
6
|
+
html: string;
|
|
7
|
+
from?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class EmailService {
|
|
11
|
+
private resend: Resend;
|
|
12
|
+
|
|
13
|
+
constructor(apiKey: string) {
|
|
14
|
+
this.resend = new Resend(apiKey);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async sendEmail(options: EmailOptions) {
|
|
18
|
+
try {
|
|
19
|
+
return await this.resend.emails.send({
|
|
20
|
+
from: options.from || 'noreply@{{DOMAIN}}',
|
|
21
|
+
to: options.to,
|
|
22
|
+
subject: options.subject,
|
|
23
|
+
html: options.html,
|
|
24
|
+
});
|
|
25
|
+
} catch (error: any) {
|
|
26
|
+
console.error(`[EmailService] Failed to send email: ${error.message}`);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "email",
|
|
3
|
+
"description": "Transactional email service with support for multiple providers.",
|
|
4
|
+
"compatibleTemplates": ["saas", "marketplace", "booking", "crm", "cms", "landing"],
|
|
5
|
+
"packageDependencies": {
|
|
6
|
+
"resend": "latest",
|
|
7
|
+
"zod": "^3.22.4"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
2
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
3
|
+
|
|
4
|
+
export class StorageService {
|
|
5
|
+
private client: S3Client;
|
|
6
|
+
|
|
7
|
+
constructor(config: { region: string; credentials: { accessKeyId: string; secretAccessKey: string } }) {
|
|
8
|
+
this.client = new S3Client(config);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async getUploadUrl(bucket: string, key: string, expiresIn: number = 3600) {
|
|
12
|
+
try {
|
|
13
|
+
const command = new PutObjectCommand({
|
|
14
|
+
Bucket: bucket,
|
|
15
|
+
Key: key,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return await getSignedUrl(this.client, command, { expiresIn });
|
|
19
|
+
} catch (error: any) {
|
|
20
|
+
console.error(`[StorageService] Failed to get upload URL: ${error.message}`);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async uploadFile(bucket: string, key: string, body: Buffer | Uint8Array | string) {
|
|
26
|
+
try {
|
|
27
|
+
const command = new PutObjectCommand({
|
|
28
|
+
Bucket: bucket,
|
|
29
|
+
Key: key,
|
|
30
|
+
Body: body,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return await this.client.send(command);
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
console.error(`[StorageService] File upload failed: ${error.message}`);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "file_upload",
|
|
3
|
+
"description": "S3-compatible file upload service.",
|
|
4
|
+
"compatibleTemplates": ["saas", "marketplace", "cms", "crm"],
|
|
5
|
+
"packageDependencies": {
|
|
6
|
+
"@aws-sdk/client-s3": "latest",
|
|
7
|
+
"@aws-sdk/s3-request-presigner": "latest",
|
|
8
|
+
"zod": "^3.22.4"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: DevForge Post-Scaffold CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
validate:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [20.x]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Setup Bun
|
|
20
|
+
uses: oven-sh/setup-bun@v1
|
|
21
|
+
with:
|
|
22
|
+
bun-version: latest
|
|
23
|
+
|
|
24
|
+
- name: Install Dependencies
|
|
25
|
+
run: bun install
|
|
26
|
+
|
|
27
|
+
- name: Run Typecheck
|
|
28
|
+
run: bun run typecheck
|
|
29
|
+
|
|
30
|
+
- name: Run Linter
|
|
31
|
+
run: bun lint
|
|
32
|
+
|
|
33
|
+
- name: Execute Tests
|
|
34
|
+
run: bun test --coverage
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "github-actions",
|
|
3
|
+
"description": "DevForge github-actions CI Pipeline generator",
|
|
4
|
+
"compatibleTemplates": [
|
|
5
|
+
"saas",
|
|
6
|
+
"marketplace",
|
|
7
|
+
"crm",
|
|
8
|
+
"ai_wrapper",
|
|
9
|
+
"booking",
|
|
10
|
+
"finance"
|
|
11
|
+
],
|
|
12
|
+
"packageDependencies": {},
|
|
13
|
+
"packageDevDependencies": {}
|
|
14
|
+
}
|