@blawness/admin-kit 0.2.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 +14 -0
- package/dist/auth/config.d.ts +3 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/auth/config.js +36 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +46 -0
- package/dist/components/admin/editor.d.ts +5 -0
- package/dist/components/admin/editor.d.ts.map +1 -0
- package/dist/components/admin/editor.js +28 -0
- package/dist/components/admin/image-upload.d.ts +15 -0
- package/dist/components/admin/image-upload.d.ts.map +1 -0
- package/dist/components/admin/image-upload.js +50 -0
- package/dist/components/admin/toast-on-param.d.ts +5 -0
- package/dist/components/admin/toast-on-param.d.ts.map +1 -0
- package/dist/components/admin/toast-on-param.js +31 -0
- package/dist/components/confirm-delete.d.ts +14 -0
- package/dist/components/confirm-delete.d.ts.map +1 -0
- package/dist/components/confirm-delete.js +32 -0
- package/dist/components/ui/button.d.ts +9 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +34 -0
- package/dist/components/ui/dialog.d.ts +18 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +37 -0
- package/dist/components/ui/input.d.ts +4 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +7 -0
- package/dist/components.d.ts +7 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +6 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +8 -0
- package/dist/db/schema.d.ts +202 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +16 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/lib/admin/media.d.ts +16 -0
- package/dist/lib/admin/media.d.ts.map +1 -0
- package/dist/lib/admin/media.js +13 -0
- package/dist/lib/admin/users.d.ts +12 -0
- package/dist/lib/admin/users.d.ts.map +1 -0
- package/dist/lib/admin/users.js +24 -0
- package/dist/lib/auth-helpers.d.ts +5 -0
- package/dist/lib/auth-helpers.d.ts.map +1 -0
- package/dist/lib/auth-helpers.js +16 -0
- package/dist/lib/db-errors.d.ts +5 -0
- package/dist/lib/db-errors.d.ts.map +1 -0
- package/dist/lib/db-errors.js +14 -0
- package/dist/lib/r2.d.ts +24 -0
- package/dist/lib/r2.d.ts.map +1 -0
- package/dist/lib/r2.js +59 -0
- package/dist/lib/sanitize.d.ts +14 -0
- package/dist/lib/sanitize.d.ts.map +1 -0
- package/dist/lib/sanitize.js +28 -0
- package/dist/lib/slug.d.ts +2 -0
- package/dist/lib/slug.d.ts.map +1 -0
- package/dist/lib/slug.js +9 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +5 -0
- package/dist/screens/login/actions.d.ts +5 -0
- package/dist/screens/login/actions.d.ts.map +1 -0
- package/dist/screens/login/actions.js +28 -0
- package/dist/screens/login/page.d.ts +3 -0
- package/dist/screens/login/page.d.ts.map +1 -0
- package/dist/screens/login/page.js +11 -0
- package/dist/screens/media/actions.d.ts +5 -0
- package/dist/screens/media/actions.d.ts.map +1 -0
- package/dist/screens/media/actions.js +32 -0
- package/dist/screens/media/lib.d.ts +9 -0
- package/dist/screens/media/lib.d.ts.map +1 -0
- package/dist/screens/media/lib.js +30 -0
- package/dist/screens/media/page.d.ts +8 -0
- package/dist/screens/media/page.d.ts.map +1 -0
- package/dist/screens/media/page.js +13 -0
- package/dist/screens/media/uploader.d.ts +2 -0
- package/dist/screens/media/uploader.d.ts.map +1 -0
- package/dist/screens/media/uploader.js +10 -0
- package/dist/screens/users/actions.d.ts +5 -0
- package/dist/screens/users/actions.d.ts.map +1 -0
- package/dist/screens/users/actions.js +70 -0
- package/dist/screens/users/page.d.ts +7 -0
- package/dist/screens/users/page.d.ts.map +1 -0
- package/dist/screens/users/page.js +22 -0
- package/dist/shell/actions.d.ts +2 -0
- package/dist/shell/actions.d.ts.map +1 -0
- package/dist/shell/actions.js +5 -0
- package/dist/shell/layout.d.ts +9 -0
- package/dist/shell/layout.d.ts.map +1 -0
- package/dist/shell/layout.js +15 -0
- package/dist/shell/sidebar.d.ts +16 -0
- package/dist/shell/sidebar.d.ts.map +1 -0
- package/dist/shell/sidebar.js +19 -0
- package/package.json +148 -0
- package/src/auth/config.ts +36 -0
- package/src/auth/index.ts +49 -0
- package/src/components/admin/editor.tsx +53 -0
- package/src/components/admin/image-upload.tsx +128 -0
- package/src/components/admin/toast-on-param.tsx +47 -0
- package/src/components/confirm-delete.tsx +96 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components.ts +17 -0
- package/src/db/index.ts +8 -0
- package/src/db/schema.ts +23 -0
- package/src/index.ts +7 -0
- package/src/lib/admin/media.ts +16 -0
- package/src/lib/admin/users.ts +31 -0
- package/src/lib/auth-helpers.ts +16 -0
- package/src/lib/db-errors.ts +18 -0
- package/src/lib/r2.ts +70 -0
- package/src/lib/sanitize.ts +29 -0
- package/src/lib/slug.ts +9 -0
- package/src/lib/utils.ts +6 -0
- package/src/screens/login/actions.ts +38 -0
- package/src/screens/login/page.tsx +48 -0
- package/src/screens/media/actions.ts +34 -0
- package/src/screens/media/lib.ts +39 -0
- package/src/screens/media/page.tsx +82 -0
- package/src/screens/media/uploader.tsx +19 -0
- package/src/screens/users/actions.ts +71 -0
- package/src/screens/users/page.tsx +128 -0
- package/src/shell/actions.ts +6 -0
- package/src/shell/layout.tsx +47 -0
- package/src/shell/sidebar.tsx +74 -0
- package/src/types/next-auth.d.ts +20 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
2
|
+
name: "users";
|
|
3
|
+
schema: undefined;
|
|
4
|
+
columns: {
|
|
5
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
6
|
+
name: "id";
|
|
7
|
+
tableName: "users";
|
|
8
|
+
dataType: "number";
|
|
9
|
+
columnType: "PgSerial";
|
|
10
|
+
data: number;
|
|
11
|
+
driverParam: number;
|
|
12
|
+
notNull: true;
|
|
13
|
+
hasDefault: true;
|
|
14
|
+
isPrimaryKey: true;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
17
|
+
enumValues: undefined;
|
|
18
|
+
baseColumn: never;
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {}>;
|
|
22
|
+
email: import("drizzle-orm/pg-core").PgColumn<{
|
|
23
|
+
name: "email";
|
|
24
|
+
tableName: "users";
|
|
25
|
+
dataType: "string";
|
|
26
|
+
columnType: "PgText";
|
|
27
|
+
data: string;
|
|
28
|
+
driverParam: string;
|
|
29
|
+
notNull: true;
|
|
30
|
+
hasDefault: false;
|
|
31
|
+
isPrimaryKey: false;
|
|
32
|
+
isAutoincrement: false;
|
|
33
|
+
hasRuntimeDefault: false;
|
|
34
|
+
enumValues: [string, ...string[]];
|
|
35
|
+
baseColumn: never;
|
|
36
|
+
identity: undefined;
|
|
37
|
+
generated: undefined;
|
|
38
|
+
}, {}, {}>;
|
|
39
|
+
name: import("drizzle-orm/pg-core").PgColumn<{
|
|
40
|
+
name: "name";
|
|
41
|
+
tableName: "users";
|
|
42
|
+
dataType: "string";
|
|
43
|
+
columnType: "PgText";
|
|
44
|
+
data: string;
|
|
45
|
+
driverParam: string;
|
|
46
|
+
notNull: true;
|
|
47
|
+
hasDefault: false;
|
|
48
|
+
isPrimaryKey: false;
|
|
49
|
+
isAutoincrement: false;
|
|
50
|
+
hasRuntimeDefault: false;
|
|
51
|
+
enumValues: [string, ...string[]];
|
|
52
|
+
baseColumn: never;
|
|
53
|
+
identity: undefined;
|
|
54
|
+
generated: undefined;
|
|
55
|
+
}, {}, {}>;
|
|
56
|
+
passwordHash: import("drizzle-orm/pg-core").PgColumn<{
|
|
57
|
+
name: "password_hash";
|
|
58
|
+
tableName: "users";
|
|
59
|
+
dataType: "string";
|
|
60
|
+
columnType: "PgText";
|
|
61
|
+
data: string;
|
|
62
|
+
driverParam: string;
|
|
63
|
+
notNull: false;
|
|
64
|
+
hasDefault: false;
|
|
65
|
+
isPrimaryKey: false;
|
|
66
|
+
isAutoincrement: false;
|
|
67
|
+
hasRuntimeDefault: false;
|
|
68
|
+
enumValues: [string, ...string[]];
|
|
69
|
+
baseColumn: never;
|
|
70
|
+
identity: undefined;
|
|
71
|
+
generated: undefined;
|
|
72
|
+
}, {}, {}>;
|
|
73
|
+
role: import("drizzle-orm/pg-core").PgColumn<{
|
|
74
|
+
name: "role";
|
|
75
|
+
tableName: "users";
|
|
76
|
+
dataType: "string";
|
|
77
|
+
columnType: "PgText";
|
|
78
|
+
data: string;
|
|
79
|
+
driverParam: string;
|
|
80
|
+
notNull: false;
|
|
81
|
+
hasDefault: true;
|
|
82
|
+
isPrimaryKey: false;
|
|
83
|
+
isAutoincrement: false;
|
|
84
|
+
hasRuntimeDefault: false;
|
|
85
|
+
enumValues: [string, ...string[]];
|
|
86
|
+
baseColumn: never;
|
|
87
|
+
identity: undefined;
|
|
88
|
+
generated: undefined;
|
|
89
|
+
}, {}, {}>;
|
|
90
|
+
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
91
|
+
name: "created_at";
|
|
92
|
+
tableName: "users";
|
|
93
|
+
dataType: "date";
|
|
94
|
+
columnType: "PgTimestamp";
|
|
95
|
+
data: Date;
|
|
96
|
+
driverParam: string;
|
|
97
|
+
notNull: false;
|
|
98
|
+
hasDefault: true;
|
|
99
|
+
isPrimaryKey: false;
|
|
100
|
+
isAutoincrement: false;
|
|
101
|
+
hasRuntimeDefault: false;
|
|
102
|
+
enumValues: undefined;
|
|
103
|
+
baseColumn: never;
|
|
104
|
+
identity: undefined;
|
|
105
|
+
generated: undefined;
|
|
106
|
+
}, {}, {}>;
|
|
107
|
+
};
|
|
108
|
+
dialect: "pg";
|
|
109
|
+
}>;
|
|
110
|
+
export declare const media: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
111
|
+
name: "media";
|
|
112
|
+
schema: undefined;
|
|
113
|
+
columns: {
|
|
114
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
115
|
+
name: "id";
|
|
116
|
+
tableName: "media";
|
|
117
|
+
dataType: "number";
|
|
118
|
+
columnType: "PgSerial";
|
|
119
|
+
data: number;
|
|
120
|
+
driverParam: number;
|
|
121
|
+
notNull: true;
|
|
122
|
+
hasDefault: true;
|
|
123
|
+
isPrimaryKey: true;
|
|
124
|
+
isAutoincrement: false;
|
|
125
|
+
hasRuntimeDefault: false;
|
|
126
|
+
enumValues: undefined;
|
|
127
|
+
baseColumn: never;
|
|
128
|
+
identity: undefined;
|
|
129
|
+
generated: undefined;
|
|
130
|
+
}, {}, {}>;
|
|
131
|
+
url: import("drizzle-orm/pg-core").PgColumn<{
|
|
132
|
+
name: "url";
|
|
133
|
+
tableName: "media";
|
|
134
|
+
dataType: "string";
|
|
135
|
+
columnType: "PgText";
|
|
136
|
+
data: string;
|
|
137
|
+
driverParam: string;
|
|
138
|
+
notNull: true;
|
|
139
|
+
hasDefault: false;
|
|
140
|
+
isPrimaryKey: false;
|
|
141
|
+
isAutoincrement: false;
|
|
142
|
+
hasRuntimeDefault: false;
|
|
143
|
+
enumValues: [string, ...string[]];
|
|
144
|
+
baseColumn: never;
|
|
145
|
+
identity: undefined;
|
|
146
|
+
generated: undefined;
|
|
147
|
+
}, {}, {}>;
|
|
148
|
+
altText: import("drizzle-orm/pg-core").PgColumn<{
|
|
149
|
+
name: "alt_text";
|
|
150
|
+
tableName: "media";
|
|
151
|
+
dataType: "string";
|
|
152
|
+
columnType: "PgText";
|
|
153
|
+
data: string;
|
|
154
|
+
driverParam: string;
|
|
155
|
+
notNull: false;
|
|
156
|
+
hasDefault: false;
|
|
157
|
+
isPrimaryKey: false;
|
|
158
|
+
isAutoincrement: false;
|
|
159
|
+
hasRuntimeDefault: false;
|
|
160
|
+
enumValues: [string, ...string[]];
|
|
161
|
+
baseColumn: never;
|
|
162
|
+
identity: undefined;
|
|
163
|
+
generated: undefined;
|
|
164
|
+
}, {}, {}>;
|
|
165
|
+
album: import("drizzle-orm/pg-core").PgColumn<{
|
|
166
|
+
name: "album";
|
|
167
|
+
tableName: "media";
|
|
168
|
+
dataType: "string";
|
|
169
|
+
columnType: "PgText";
|
|
170
|
+
data: string;
|
|
171
|
+
driverParam: string;
|
|
172
|
+
notNull: false;
|
|
173
|
+
hasDefault: false;
|
|
174
|
+
isPrimaryKey: false;
|
|
175
|
+
isAutoincrement: false;
|
|
176
|
+
hasRuntimeDefault: false;
|
|
177
|
+
enumValues: [string, ...string[]];
|
|
178
|
+
baseColumn: never;
|
|
179
|
+
identity: undefined;
|
|
180
|
+
generated: undefined;
|
|
181
|
+
}, {}, {}>;
|
|
182
|
+
uploadedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
183
|
+
name: "uploaded_at";
|
|
184
|
+
tableName: "media";
|
|
185
|
+
dataType: "date";
|
|
186
|
+
columnType: "PgTimestamp";
|
|
187
|
+
data: Date;
|
|
188
|
+
driverParam: string;
|
|
189
|
+
notNull: false;
|
|
190
|
+
hasDefault: true;
|
|
191
|
+
isPrimaryKey: false;
|
|
192
|
+
isAutoincrement: false;
|
|
193
|
+
hasRuntimeDefault: false;
|
|
194
|
+
enumValues: undefined;
|
|
195
|
+
baseColumn: never;
|
|
196
|
+
identity: undefined;
|
|
197
|
+
generated: undefined;
|
|
198
|
+
}, {}, {}>;
|
|
199
|
+
};
|
|
200
|
+
dialect: "pg";
|
|
201
|
+
}>;
|
|
202
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAOhB,CAAC;AAEH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMhB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { pgTable, serial, text, timestamp, } from "drizzle-orm/pg-core";
|
|
2
|
+
export const users = pgTable("users", {
|
|
3
|
+
id: serial("id").primaryKey(),
|
|
4
|
+
email: text("email").notNull().unique(),
|
|
5
|
+
name: text("name").notNull(),
|
|
6
|
+
passwordHash: text("password_hash"),
|
|
7
|
+
role: text("role").default("editor"),
|
|
8
|
+
createdAt: timestamp("created_at").defaultNow(),
|
|
9
|
+
});
|
|
10
|
+
export const media = pgTable("media", {
|
|
11
|
+
id: serial("id").primaryKey(),
|
|
12
|
+
url: text("url").notNull(),
|
|
13
|
+
altText: text("alt_text"),
|
|
14
|
+
album: text("album"),
|
|
15
|
+
uploadedAt: timestamp("uploaded_at").defaultNow(),
|
|
16
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { cn } from "./lib/utils";
|
|
2
|
+
export { slugify } from "./lib/slug";
|
|
3
|
+
export { sanitizeHtml } from "./lib/sanitize";
|
|
4
|
+
export * from "./lib/db-errors";
|
|
5
|
+
export * from "./lib/r2";
|
|
6
|
+
export * from "./db/schema";
|
|
7
|
+
export { db } from "./db/index";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,EAAE,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function listMedia(): Promise<{
|
|
2
|
+
id: number;
|
|
3
|
+
url: string;
|
|
4
|
+
altText: string | null;
|
|
5
|
+
album: string | null;
|
|
6
|
+
uploadedAt: Date | null;
|
|
7
|
+
}[]>;
|
|
8
|
+
export declare function getMediaById(id: number): Promise<{
|
|
9
|
+
id: number;
|
|
10
|
+
url: string;
|
|
11
|
+
altText: string | null;
|
|
12
|
+
album: string | null;
|
|
13
|
+
uploadedAt: Date | null;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function deleteMediaRow(id: number): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=media.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../../src/lib/admin/media.ts"],"names":[],"mappings":"AAIA,wBAAsB,SAAS;;;;;;KAE9B;AAED,wBAAsB,YAAY,CAAC,EAAE,EAAE,MAAM;;;;;;GAG5C;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM,iBAE9C"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { db } from "../../db/index";
|
|
2
|
+
import { media } from "../../db/schema";
|
|
3
|
+
import { desc, eq } from "drizzle-orm";
|
|
4
|
+
export async function listMedia() {
|
|
5
|
+
return db.select().from(media).orderBy(desc(media.uploadedAt));
|
|
6
|
+
}
|
|
7
|
+
export async function getMediaById(id) {
|
|
8
|
+
const [row] = await db.select().from(media).where(eq(media.id, id)).limit(1);
|
|
9
|
+
return row ?? null;
|
|
10
|
+
}
|
|
11
|
+
export async function deleteMediaRow(id) {
|
|
12
|
+
await db.delete(media).where(eq(media.id, id));
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function listUsers(): Promise<{
|
|
2
|
+
id: number;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
role: string | null;
|
|
6
|
+
}[]>;
|
|
7
|
+
export type UserRole = "admin" | "editor";
|
|
8
|
+
export declare function createUser(email: string, name: string, password: string, role: UserRole): Promise<void>;
|
|
9
|
+
export declare function updateUserPassword(id: number, password: string): Promise<void>;
|
|
10
|
+
export declare function updateUserRole(id: number, role: UserRole): Promise<void>;
|
|
11
|
+
export declare function deleteUser(id: number): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=users.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../../src/lib/admin/users.ts"],"names":[],"mappings":"AAKA,wBAAsB,SAAS;;;;;KAK9B;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1C,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,iBAG7F;AAED,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,iBAGpE;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,iBAE9D;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM,iBAE1C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { db } from "../../db/index";
|
|
2
|
+
import { users } from "../../db/schema";
|
|
3
|
+
import { asc, eq } from "drizzle-orm";
|
|
4
|
+
import { hash } from "bcryptjs";
|
|
5
|
+
export async function listUsers() {
|
|
6
|
+
return db
|
|
7
|
+
.select({ id: users.id, email: users.email, name: users.name, role: users.role })
|
|
8
|
+
.from(users)
|
|
9
|
+
.orderBy(asc(users.email));
|
|
10
|
+
}
|
|
11
|
+
export async function createUser(email, name, password, role) {
|
|
12
|
+
const passwordHash = await hash(password, 12);
|
|
13
|
+
await db.insert(users).values({ email, name, passwordHash, role });
|
|
14
|
+
}
|
|
15
|
+
export async function updateUserPassword(id, password) {
|
|
16
|
+
const passwordHash = await hash(password, 12);
|
|
17
|
+
await db.update(users).set({ passwordHash }).where(eq(users.id, id));
|
|
18
|
+
}
|
|
19
|
+
export async function updateUserRole(id, role) {
|
|
20
|
+
await db.update(users).set({ role }).where(eq(users.id, id));
|
|
21
|
+
}
|
|
22
|
+
export async function deleteUser(id) {
|
|
23
|
+
await db.delete(users).where(eq(users.id, id));
|
|
24
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Any authenticated user (admin or editor). Returns the session. */
|
|
2
|
+
export declare function requireUser(): Promise<import("next-auth").Session>;
|
|
3
|
+
/** Admin only. Editors are sent to the dashboard. */
|
|
4
|
+
export declare function requireAdmin(): Promise<import("next-auth").Session>;
|
|
5
|
+
//# sourceMappingURL=auth-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/auth-helpers.ts"],"names":[],"mappings":"AAGA,qEAAqE;AACrE,wBAAsB,WAAW,yCAIhC;AAED,qDAAqD;AACrD,wBAAsB,YAAY,yCAIjC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
import { auth } from "../auth/index";
|
|
3
|
+
/** Any authenticated user (admin or editor). Returns the session. */
|
|
4
|
+
export async function requireUser() {
|
|
5
|
+
const session = await auth();
|
|
6
|
+
if (!session?.user)
|
|
7
|
+
redirect("/admin/login");
|
|
8
|
+
return session;
|
|
9
|
+
}
|
|
10
|
+
/** Admin only. Editors are sent to the dashboard. */
|
|
11
|
+
export async function requireAdmin() {
|
|
12
|
+
const session = await requireUser();
|
|
13
|
+
if (session.user.role !== "admin")
|
|
14
|
+
redirect("/admin");
|
|
15
|
+
return session;
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** True when a Postgres error is a unique-constraint violation (SQLSTATE 23505). */
|
|
2
|
+
export declare function isUniqueViolation(error: unknown): boolean;
|
|
3
|
+
/** True when a Postgres error is a foreign-key violation (SQLSTATE 23503). */
|
|
4
|
+
export declare function isForeignKeyViolation(error: unknown): boolean;
|
|
5
|
+
//# sourceMappingURL=db-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-errors.d.ts","sourceRoot":"","sources":["../../src/lib/db-errors.ts"],"names":[],"mappings":"AASA,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED,8EAA8E;AAC9E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE7D"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function hasSqlState(error, code) {
|
|
2
|
+
return (typeof error === "object" &&
|
|
3
|
+
error !== null &&
|
|
4
|
+
"code" in error &&
|
|
5
|
+
error.code === code);
|
|
6
|
+
}
|
|
7
|
+
/** True when a Postgres error is a unique-constraint violation (SQLSTATE 23505). */
|
|
8
|
+
export function isUniqueViolation(error) {
|
|
9
|
+
return hasSqlState(error, "23505");
|
|
10
|
+
}
|
|
11
|
+
/** True when a Postgres error is a foreign-key violation (SQLSTATE 23503). */
|
|
12
|
+
export function isForeignKeyViolation(error) {
|
|
13
|
+
return hasSqlState(error, "23503");
|
|
14
|
+
}
|
package/dist/lib/r2.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
export declare const R2_BUCKET: string;
|
|
3
|
+
/** Base URL publik untuk menyajikan objek (r2.dev atau custom domain), tanpa trailing slash. */
|
|
4
|
+
export declare const R2_PUBLIC_URL: string;
|
|
5
|
+
export declare const r2: S3Client;
|
|
6
|
+
/**
|
|
7
|
+
* Resize + kompres gambar lalu unggah ke R2.
|
|
8
|
+
* Apa pun ukuran input, hasil disimpan maksimal 1600px lebar, JPEG q80.
|
|
9
|
+
* Mengembalikan URL publik yang siap disimpan ke kolom `featured_image`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function uploadImage(input: Buffer,
|
|
12
|
+
/** key/nama objek di bucket, tanpa ekstensi — mis. "berita/slug-artikel" */
|
|
13
|
+
keyBase: string): Promise<{
|
|
14
|
+
url: string;
|
|
15
|
+
key: string;
|
|
16
|
+
size: number;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* Hapus objek dari R2 berdasarkan URL publiknya. Hanya menghapus objek yang
|
|
20
|
+
* memang berada di bawah R2_PUBLIC_URL — URL eksternal/seed diabaikan dengan
|
|
21
|
+
* aman (return false). Kegagalan jaringan dibiarkan melempar ke pemanggil.
|
|
22
|
+
*/
|
|
23
|
+
export declare function deleteObjectByUrl(url: string): Promise<boolean>;
|
|
24
|
+
//# sourceMappingURL=r2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../../src/lib/r2.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAyC,MAAM,oBAAoB,CAAC;AAUrF,eAAO,MAAM,SAAS,QAAsC,CAAC;AAC7D,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAAuD,CAAC;AAElF,eAAO,MAAM,EAAE,UAOb,CAAC;AAEH;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM;AACb,4EAA4E;AAC5E,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAwBrD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMrE"}
|
package/dist/lib/r2.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { S3Client, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import sharp from "sharp";
|
|
3
|
+
const endpoint = process.env.R2_ENDPOINT;
|
|
4
|
+
const accessKeyId = process.env.R2_ACCESS_KEY_ID;
|
|
5
|
+
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
|
|
6
|
+
if (!endpoint)
|
|
7
|
+
throw new Error("admin-kit: R2_ENDPOINT env var is required");
|
|
8
|
+
if (!accessKeyId || !secretAccessKey)
|
|
9
|
+
throw new Error("admin-kit: R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY env vars are required");
|
|
10
|
+
export const R2_BUCKET = process.env.R2_BUCKET ?? "lipan-ri";
|
|
11
|
+
/** Base URL publik untuk menyajikan objek (r2.dev atau custom domain), tanpa trailing slash. */
|
|
12
|
+
export const R2_PUBLIC_URL = (process.env.R2_PUBLIC_URL ?? "").replace(/\/$/, "");
|
|
13
|
+
export const r2 = new S3Client({
|
|
14
|
+
region: "auto",
|
|
15
|
+
endpoint,
|
|
16
|
+
credentials: accessKeyId && secretAccessKey
|
|
17
|
+
? { accessKeyId, secretAccessKey }
|
|
18
|
+
: undefined,
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Resize + kompres gambar lalu unggah ke R2.
|
|
22
|
+
* Apa pun ukuran input, hasil disimpan maksimal 1600px lebar, JPEG q80.
|
|
23
|
+
* Mengembalikan URL publik yang siap disimpan ke kolom `featured_image`.
|
|
24
|
+
*/
|
|
25
|
+
export async function uploadImage(input,
|
|
26
|
+
/** key/nama objek di bucket, tanpa ekstensi — mis. "berita/slug-artikel" */
|
|
27
|
+
keyBase) {
|
|
28
|
+
const optimized = await sharp(input)
|
|
29
|
+
.rotate() // hormati orientasi EXIF
|
|
30
|
+
.resize({ width: 1600, withoutEnlargement: true })
|
|
31
|
+
.jpeg({ quality: 80, mozjpeg: true })
|
|
32
|
+
.toBuffer();
|
|
33
|
+
const key = `${keyBase.replace(/^\/+/, "")}.jpg`;
|
|
34
|
+
await r2.send(new PutObjectCommand({
|
|
35
|
+
Bucket: R2_BUCKET,
|
|
36
|
+
Key: key,
|
|
37
|
+
Body: optimized,
|
|
38
|
+
ContentType: "image/jpeg",
|
|
39
|
+
CacheControl: "public, max-age=31536000, immutable",
|
|
40
|
+
}));
|
|
41
|
+
if (!R2_PUBLIC_URL) {
|
|
42
|
+
throw new Error("R2_PUBLIC_URL belum di-set — tidak bisa menyusun URL publik.");
|
|
43
|
+
}
|
|
44
|
+
return { url: `${R2_PUBLIC_URL}/${key}`, key, size: optimized.length };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Hapus objek dari R2 berdasarkan URL publiknya. Hanya menghapus objek yang
|
|
48
|
+
* memang berada di bawah R2_PUBLIC_URL — URL eksternal/seed diabaikan dengan
|
|
49
|
+
* aman (return false). Kegagalan jaringan dibiarkan melempar ke pemanggil.
|
|
50
|
+
*/
|
|
51
|
+
export async function deleteObjectByUrl(url) {
|
|
52
|
+
if (!R2_PUBLIC_URL || !url.startsWith(`${R2_PUBLIC_URL}/`))
|
|
53
|
+
return false;
|
|
54
|
+
const key = url.slice(R2_PUBLIC_URL.length + 1);
|
|
55
|
+
if (!key)
|
|
56
|
+
return false;
|
|
57
|
+
await r2.send(new DeleteObjectCommand({ Bucket: R2_BUCKET, Key: key }));
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize editor HTML before persisting (content is rendered via
|
|
3
|
+
* dangerouslySetInnerHTML on the public site).
|
|
4
|
+
*
|
|
5
|
+
* Uses `sanitize-html` (pure JS, htmlparser2 — no jsdom) so it loads in the
|
|
6
|
+
* Vercel serverless runtime; `isomorphic-dompurify`/jsdom failed to load there
|
|
7
|
+
* ("Failed to load external module" → /admin/posts 500).
|
|
8
|
+
*
|
|
9
|
+
* Security posture (unchanged): strict tag allowlist, no `target` (avoids
|
|
10
|
+
* tab-napping), only http(s)/mailto + root-relative URLs, and protocol-relative
|
|
11
|
+
* `//host` URLs are rejected.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sanitizeHtml(dirty: string): string;
|
|
14
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/lib/sanitize.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAclD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import sanitizeHtmlLib from "sanitize-html";
|
|
2
|
+
/**
|
|
3
|
+
* Sanitize editor HTML before persisting (content is rendered via
|
|
4
|
+
* dangerouslySetInnerHTML on the public site).
|
|
5
|
+
*
|
|
6
|
+
* Uses `sanitize-html` (pure JS, htmlparser2 — no jsdom) so it loads in the
|
|
7
|
+
* Vercel serverless runtime; `isomorphic-dompurify`/jsdom failed to load there
|
|
8
|
+
* ("Failed to load external module" → /admin/posts 500).
|
|
9
|
+
*
|
|
10
|
+
* Security posture (unchanged): strict tag allowlist, no `target` (avoids
|
|
11
|
+
* tab-napping), only http(s)/mailto + root-relative URLs, and protocol-relative
|
|
12
|
+
* `//host` URLs are rejected.
|
|
13
|
+
*/
|
|
14
|
+
export function sanitizeHtml(dirty) {
|
|
15
|
+
return sanitizeHtmlLib(dirty, {
|
|
16
|
+
allowedTags: [
|
|
17
|
+
"p", "br", "strong", "em", "u", "s", "h2", "h3", "h4",
|
|
18
|
+
"ul", "ol", "li", "blockquote", "a", "img", "figure", "figcaption",
|
|
19
|
+
],
|
|
20
|
+
allowedAttributes: {
|
|
21
|
+
a: ["href", "rel", "title"],
|
|
22
|
+
img: ["src", "alt", "title"],
|
|
23
|
+
},
|
|
24
|
+
allowedSchemes: ["http", "https", "mailto"],
|
|
25
|
+
allowedSchemesByTag: { img: ["http", "https"] },
|
|
26
|
+
allowProtocolRelative: false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slug.d.ts","sourceRoot":"","sources":["../../src/lib/slug.ts"],"names":[],"mappings":"AAAA,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ7C"}
|
package/dist/lib/slug.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,MAAM,CAAA;AAG5C,wBAAgB,EAAE,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,UAEzC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../../src/screens/login/actions.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,wBAAsB,WAAW,CAC/B,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,UAAU,CAAC,CA2BrB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
import { signIn } from "../../auth/index";
|
|
4
|
+
export async function loginAction(_prev, formData) {
|
|
5
|
+
try {
|
|
6
|
+
const result = await signIn("credentials", {
|
|
7
|
+
email: formData.get("email"),
|
|
8
|
+
password: formData.get("password"),
|
|
9
|
+
redirect: false,
|
|
10
|
+
});
|
|
11
|
+
// Auth.js returns the redirect URL when redirect:false
|
|
12
|
+
// On failure it returns a URL with ?error= param
|
|
13
|
+
if (typeof result === "string" && result.includes("error=")) {
|
|
14
|
+
return { error: "Email atau password salah." };
|
|
15
|
+
}
|
|
16
|
+
redirect("/admin");
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// Catch CredentialsSignin and other auth errors thrown by next-auth
|
|
20
|
+
// NEXT_REDIRECT thrown by redirect() must propagate
|
|
21
|
+
if (error instanceof Error &&
|
|
22
|
+
"digest" in error &&
|
|
23
|
+
String(error.digest).startsWith("NEXT_REDIRECT")) {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
return { error: "Email atau password salah." };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../src/screens/login/page.tsx"],"names":[],"mappings":"AAOA,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAEvC,MAAM,CAAC,OAAO,UAAU,WAAW,gCAsClC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useActionState } from "react";
|
|
4
|
+
import { loginAction } from "./actions";
|
|
5
|
+
import { Button } from "../../components/ui/button";
|
|
6
|
+
import { Input } from "../../components/ui/input";
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
export default function LoginScreen() {
|
|
9
|
+
const [state, action, pending] = useActionState(loginAction, {});
|
|
10
|
+
return (_jsx("div", { className: "min-h-screen flex items-center justify-center bg-navy-50 px-4", children: _jsxs("form", { action: action, className: "w-full max-w-sm bg-white rounded-2xl shadow-xl ring-1 ring-navy-100 p-8 space-y-4", children: [_jsx("h1", { className: "font-heading text-2xl font-bold text-navy-900", children: "Masuk Admin" }), _jsxs("div", { className: "space-y-1", children: [_jsx("label", { htmlFor: "email", className: "text-sm font-medium text-navy-900", children: "Email" }), _jsx(Input, { id: "email", name: "email", type: "email", required: true, autoComplete: "email" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("label", { htmlFor: "password", className: "text-sm font-medium text-navy-900", children: "Password" }), _jsx(Input, { id: "password", name: "password", type: "password", required: true, autoComplete: "current-password" })] }), state.error && (_jsx("p", { className: "text-sm text-red-600", role: "alert", children: state.error })), _jsx(Button, { type: "submit", className: "w-full", disabled: pending, children: pending ? "Memproses…" : "Masuk" })] }) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../../src/screens/media/actions.ts"],"names":[],"mappings":"AAWA,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBrG"}
|