@orhancodestudio/ocsm-core 0.1.0-alpha.1

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +47 -0
  3. package/package.json +53 -0
  4. package/src/admin/admin.module.css +1312 -0
  5. package/src/admin/admin.types.ts +85 -0
  6. package/src/admin/components/access-denied.tsx +12 -0
  7. package/src/admin/components/admin-shell.tsx +168 -0
  8. package/src/admin/components/content-list-view.tsx +83 -0
  9. package/src/admin/components/dashboard-view.tsx +113 -0
  10. package/src/admin/components/data-table.tsx +80 -0
  11. package/src/admin/components/document-delete-button.tsx +44 -0
  12. package/src/admin/components/icons.tsx +150 -0
  13. package/src/admin/components/modal.tsx +78 -0
  14. package/src/admin/components/page-builder.tsx +1334 -0
  15. package/src/admin/components/settings-view.tsx +334 -0
  16. package/src/admin/components/sign-out-button.tsx +22 -0
  17. package/src/admin/components/system-view.tsx +77 -0
  18. package/src/admin/components/users-panel.tsx +321 -0
  19. package/src/admin/index.ts +20 -0
  20. package/src/admin/ocsm-admin.tsx +259 -0
  21. package/src/auth/authenticate.ts +76 -0
  22. package/src/auth/index.ts +9 -0
  23. package/src/auth/password.ts +22 -0
  24. package/src/auth/permissions.ts +27 -0
  25. package/src/auth/session.ts +103 -0
  26. package/src/blocks/block-renderer.tsx +428 -0
  27. package/src/blocks/block.types.ts +401 -0
  28. package/src/blocks/index.ts +15 -0
  29. package/src/blocks/markdown.tsx +11 -0
  30. package/src/config/config.schema.ts +28 -0
  31. package/src/config/config.types.ts +16 -0
  32. package/src/config/define-config.ts +19 -0
  33. package/src/config/index.ts +13 -0
  34. package/src/config/resolve-config.ts +10 -0
  35. package/src/content/content-repository.ts +66 -0
  36. package/src/content/content-store.interface.ts +23 -0
  37. package/src/content/content.types.ts +25 -0
  38. package/src/content/create-content-store.ts +18 -0
  39. package/src/content/frontmatter.ts +25 -0
  40. package/src/content/index.ts +12 -0
  41. package/src/index.ts +10 -0
  42. package/src/layout/index.ts +1 -0
  43. package/src/layout/layout-store.ts +27 -0
  44. package/src/roles/index.ts +10 -0
  45. package/src/roles/role-store.ts +95 -0
  46. package/src/roles/role.types.ts +86 -0
  47. package/src/server/create-ocsm.ts +67 -0
  48. package/src/server/documents.ts +28 -0
  49. package/src/server/index.ts +59 -0
  50. package/src/server/render-mdx.tsx +14 -0
  51. package/src/storage/create-file-backend.ts +26 -0
  52. package/src/storage/file-backend.ts +26 -0
  53. package/src/storage/fs-file-backend.ts +43 -0
  54. package/src/storage/github-file-backend.ts +97 -0
  55. package/src/storage/index.ts +8 -0
  56. package/src/storage/json-store.ts +23 -0
  57. package/src/theme/css.ts +28 -0
  58. package/src/theme/index.ts +8 -0
  59. package/src/theme/theme-store.ts +19 -0
  60. package/src/theme/theme.types.ts +53 -0
  61. package/src/types/css-modules.d.ts +4 -0
  62. package/src/update/check-for-updates.ts +50 -0
  63. package/src/update/index.ts +1 -0
  64. package/src/users/index.ts +6 -0
  65. package/src/users/user-store.ts +120 -0
  66. package/src/users/user.types.ts +18 -0
  67. package/src/version.ts +11 -0
@@ -0,0 +1,334 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { type FormEvent, useState, useTransition } from "react";
5
+ import {
6
+ ALL_PERMISSIONS,
7
+ type OcsmPermission,
8
+ type OcsmRole,
9
+ SYSTEM_ADMIN_ROLE_ID,
10
+ } from "../../roles/role.types";
11
+ import type {
12
+ CreateRoleAction,
13
+ DeleteRoleAction,
14
+ UpdateRoleAction,
15
+ } from "../admin.types";
16
+ import styles from "../admin.module.css";
17
+ import { DataTable, type DataTableColumn } from "./data-table";
18
+ import { IconPencil, IconPlus, IconTrash } from "./icons";
19
+ import { Modal } from "./modal";
20
+
21
+ export interface SettingsViewProps {
22
+ roles: OcsmRole[];
23
+ createRole: CreateRoleAction;
24
+ updateRole: UpdateRoleAction;
25
+ deleteRole: DeleteRoleAction;
26
+ }
27
+
28
+ type Tab = "roles";
29
+
30
+ const TABS: Array<{ key: Tab; label: string }> = [
31
+ { key: "roles", label: "Roller ve Yetkiler" },
32
+ ];
33
+
34
+ export function SettingsView({
35
+ roles,
36
+ createRole,
37
+ updateRole,
38
+ deleteRole,
39
+ }: SettingsViewProps) {
40
+ const [tab, setTab] = useState<Tab>("roles");
41
+
42
+ return (
43
+ <div>
44
+ <div className={styles.pageHeader}>
45
+ <div>
46
+ <h1 className={styles.pageTitle}>Yapılandırma</h1>
47
+ <p className={styles.pageSubtitle}>
48
+ Sistem genelindeki ayarları buradan yönetin.
49
+ </p>
50
+ </div>
51
+ </div>
52
+
53
+ <div className={styles.tabs}>
54
+ {TABS.map((entry) => (
55
+ <button
56
+ key={entry.key}
57
+ type="button"
58
+ className={`${styles.tab} ${tab === entry.key ? styles.tabActive : ""}`}
59
+ onClick={() => setTab(entry.key)}
60
+ >
61
+ {entry.label}
62
+ </button>
63
+ ))}
64
+ </div>
65
+
66
+ {tab === "roles" ? (
67
+ <RolesTab
68
+ roles={roles}
69
+ createRole={createRole}
70
+ updateRole={updateRole}
71
+ deleteRole={deleteRole}
72
+ />
73
+ ) : null}
74
+ </div>
75
+ );
76
+ }
77
+
78
+ type RoleDialog = { mode: "create" } | { mode: "edit"; role: OcsmRole } | null;
79
+
80
+ function RolesTab({
81
+ roles,
82
+ createRole,
83
+ updateRole,
84
+ deleteRole,
85
+ }: {
86
+ roles: OcsmRole[];
87
+ createRole: CreateRoleAction;
88
+ updateRole: UpdateRoleAction;
89
+ deleteRole: DeleteRoleAction;
90
+ }) {
91
+ const router = useRouter();
92
+ const [dialog, setDialog] = useState<RoleDialog>(null);
93
+ const [pending, startTransition] = useTransition();
94
+
95
+ function onDelete(role: OcsmRole) {
96
+ if (!window.confirm(`"${role.name}" rolü silinsin mi?`)) return;
97
+ startTransition(async () => {
98
+ const result = await deleteRole(role.id);
99
+ if (!result.ok && result.message) window.alert(result.message);
100
+ router.refresh();
101
+ });
102
+ }
103
+
104
+ const columns: DataTableColumn<OcsmRole>[] = [
105
+ {
106
+ key: "name",
107
+ header: "Rol",
108
+ cell: (role) => (
109
+ <>
110
+ {role.name}
111
+ {role.system ? (
112
+ <span className={styles.badge} style={{ marginLeft: 8 }}>
113
+ sistem
114
+ </span>
115
+ ) : null}
116
+ </>
117
+ ),
118
+ },
119
+ {
120
+ key: "perms",
121
+ header: "Yetkiler",
122
+ cell: (role) => (
123
+ <span className={styles.cardHint}>
124
+ {role.permissions.length} / {ALL_PERMISSIONS.length} yetki
125
+ </span>
126
+ ),
127
+ },
128
+ {
129
+ key: "actions",
130
+ header: "",
131
+ align: "right",
132
+ cell: (role) => (
133
+ <div className={styles.tableActions}>
134
+ <button
135
+ type="button"
136
+ className={`${styles.btn} ${styles.btnSm}`}
137
+ onClick={() => setDialog({ mode: "edit", role })}
138
+ title="Düzenle"
139
+ >
140
+ <IconPencil width={14} height={14} />
141
+ </button>
142
+ <button
143
+ type="button"
144
+ className={`${styles.btn} ${styles.btnSm} ${styles.btnDanger}`}
145
+ onClick={() => onDelete(role)}
146
+ disabled={!!role.system}
147
+ title={role.system ? "Sistem rolü silinemez" : "Sil"}
148
+ >
149
+ <IconTrash width={14} height={14} />
150
+ </button>
151
+ </div>
152
+ ),
153
+ },
154
+ ];
155
+
156
+ return (
157
+ <div>
158
+ <div
159
+ style={{
160
+ display: "flex",
161
+ justifyContent: "flex-end",
162
+ marginBottom: 14,
163
+ }}
164
+ >
165
+ <button
166
+ type="button"
167
+ className={`${styles.btn} ${styles.btnPrimary}`}
168
+ onClick={() => setDialog({ mode: "create" })}
169
+ >
170
+ <IconPlus width={16} height={16} />
171
+ Yeni rol
172
+ </button>
173
+ </div>
174
+
175
+ <DataTable
176
+ columns={columns}
177
+ rows={roles}
178
+ rowKey={(role) => role.id}
179
+ emptyText="Henüz rol yok."
180
+ />
181
+
182
+ {dialog?.mode === "create" ? (
183
+ <RoleDialogForm
184
+ title="Yeni rol"
185
+ pending={pending}
186
+ onClose={() => setDialog(null)}
187
+ onSubmit={(values, done) =>
188
+ startTransition(async () => {
189
+ const result = await createRole(values);
190
+ done(result.ok, result.message);
191
+ if (result.ok) {
192
+ setDialog(null);
193
+ router.refresh();
194
+ }
195
+ })
196
+ }
197
+ />
198
+ ) : null}
199
+
200
+ {dialog?.mode === "edit" ? (
201
+ <RoleDialogForm
202
+ title="Rolü düzenle"
203
+ pending={pending}
204
+ initial={dialog.role}
205
+ locked={dialog.role.id === SYSTEM_ADMIN_ROLE_ID}
206
+ onClose={() => setDialog(null)}
207
+ onSubmit={(values, done) =>
208
+ startTransition(async () => {
209
+ const result = await updateRole(dialog.role.id, values);
210
+ done(result.ok, result.message);
211
+ if (result.ok) {
212
+ setDialog(null);
213
+ router.refresh();
214
+ }
215
+ })
216
+ }
217
+ />
218
+ ) : null}
219
+ </div>
220
+ );
221
+ }
222
+
223
+ function RoleDialogForm({
224
+ title,
225
+ initial,
226
+ locked,
227
+ pending,
228
+ onClose,
229
+ onSubmit,
230
+ }: {
231
+ title: string;
232
+ initial?: OcsmRole;
233
+ locked?: boolean;
234
+ pending: boolean;
235
+ onClose: () => void;
236
+ onSubmit: (
237
+ values: { name: string; permissions: OcsmPermission[] },
238
+ done: (ok: boolean, message?: string) => void,
239
+ ) => void;
240
+ }) {
241
+ const [name, setName] = useState(initial?.name ?? "");
242
+ const [permissions, setPermissions] = useState<OcsmPermission[]>(
243
+ initial?.permissions ?? ["content:read"],
244
+ );
245
+ const [error, setError] = useState<string | null>(null);
246
+
247
+ function toggle(perm: OcsmPermission) {
248
+ if (locked) return;
249
+ setPermissions((current) =>
250
+ current.includes(perm)
251
+ ? current.filter((p) => p !== perm)
252
+ : [...current, perm],
253
+ );
254
+ }
255
+
256
+ function submit(event: FormEvent) {
257
+ event.preventDefault();
258
+ setError(null);
259
+ onSubmit({ name, permissions }, (ok, message) => {
260
+ if (!ok) setError(message ?? "İşlem başarısız");
261
+ });
262
+ }
263
+
264
+ return (
265
+ <Modal
266
+ open
267
+ title={title}
268
+ width={560}
269
+ onClose={onClose}
270
+ footer={
271
+ <>
272
+ <button
273
+ type="button"
274
+ className={styles.btn}
275
+ onClick={onClose}
276
+ disabled={pending}
277
+ >
278
+ Vazgeç
279
+ </button>
280
+ <button
281
+ type="submit"
282
+ form="ocsm-role-form"
283
+ className={`${styles.btn} ${styles.btnPrimary}`}
284
+ disabled={pending}
285
+ >
286
+ {pending ? "Kaydediliyor…" : "Kaydet"}
287
+ </button>
288
+ </>
289
+ }
290
+ >
291
+ <form id="ocsm-role-form" onSubmit={submit}>
292
+ <div className={styles.field}>
293
+ <label className={styles.label} htmlFor="r-name">
294
+ Rol adı
295
+ </label>
296
+ <input
297
+ id="r-name"
298
+ className={styles.input}
299
+ value={name}
300
+ onChange={(e) => setName(e.target.value)}
301
+ required
302
+ />
303
+ </div>
304
+
305
+ <span className={styles.label}>Yetkiler</span>
306
+ {locked ? (
307
+ <p className={styles.help}>
308
+ Bu sistem rolü her zaman tüm yetkilere sahiptir ve değiştirilemez.
309
+ </p>
310
+ ) : null}
311
+ <div className={styles.permList}>
312
+ {ALL_PERMISSIONS.map((perm) => {
313
+ const checked = locked || permissions.includes(perm.key);
314
+ return (
315
+ <label key={perm.key} className={styles.permItem}>
316
+ <input
317
+ type="checkbox"
318
+ checked={checked}
319
+ disabled={locked}
320
+ onChange={() => toggle(perm.key)}
321
+ />
322
+ <span>
323
+ <span className={styles.permLabel}>{perm.label}</span>
324
+ <span className={styles.permDesc}>{perm.description}</span>
325
+ </span>
326
+ </label>
327
+ );
328
+ })}
329
+ </div>
330
+ {error ? <p className={styles.errorText}>{error}</p> : null}
331
+ </form>
332
+ </Modal>
333
+ );
334
+ }
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ import { useTransition } from "react";
4
+ import type { SignOutAction } from "../admin.types";
5
+ import styles from "../admin.module.css";
6
+ import { IconLogout } from "./icons";
7
+
8
+ export function SignOutButton({ signOut }: { signOut: SignOutAction }) {
9
+ const [pending, startTransition] = useTransition();
10
+
11
+ return (
12
+ <button
13
+ type="button"
14
+ className={`${styles.btn} ${styles.btnSm}`}
15
+ disabled={pending}
16
+ onClick={() => startTransition(() => signOut())}
17
+ >
18
+ <IconLogout width={15} height={15} />
19
+ {pending ? "Çıkılıyor…" : "Çıkış"}
20
+ </button>
21
+ );
22
+ }
@@ -0,0 +1,77 @@
1
+ import { OCSM_PACKAGE_NAME } from "../../version";
2
+ import styles from "../admin.module.css";
3
+
4
+ export interface SystemViewProps {
5
+ version: string;
6
+ updateLatest: string | null;
7
+ storageBackend: "filesystem" | "github";
8
+ userCount: number;
9
+ collectionCount: number;
10
+ }
11
+
12
+ export function SystemView({
13
+ version,
14
+ updateLatest,
15
+ storageBackend,
16
+ userCount,
17
+ collectionCount,
18
+ }: SystemViewProps) {
19
+ const rows: Array<{ label: string; value: React.ReactNode }> = [
20
+ { label: "Paket", value: <code>{OCSM_PACKAGE_NAME}</code> },
21
+ { label: "Sürüm", value: `v${version}` },
22
+ {
23
+ label: "Güncelleme",
24
+ value: updateLatest ? (
25
+ <span className={styles.badgeWarn} style={{ padding: "2px 8px" }}>
26
+ v{updateLatest} mevcut
27
+ </span>
28
+ ) : (
29
+ <span className={styles.badgeOk} style={{ padding: "2px 8px" }}>
30
+ Güncel
31
+ </span>
32
+ ),
33
+ },
34
+ {
35
+ label: "Depolama",
36
+ value:
37
+ storageBackend === "github"
38
+ ? "GitHub (commit ile)"
39
+ : "Yerel dosya sistemi",
40
+ },
41
+ { label: "Kullanıcı sayısı", value: userCount },
42
+ { label: "Koleksiyon sayısı", value: collectionCount },
43
+ ];
44
+
45
+ return (
46
+ <div>
47
+ <div className={styles.pageHeader}>
48
+ <div>
49
+ <h1 className={styles.pageTitle}>Sistem</h1>
50
+ <p className={styles.pageSubtitle}>OCS Management kurulum bilgileri</p>
51
+ </div>
52
+ </div>
53
+
54
+ {updateLatest ? (
55
+ <div className={styles.notice}>
56
+ Yeni bir sürüm yayınlandı: <strong>v{updateLatest}</strong>. Çekirdek
57
+ paketini güncelleyerek son sürüme geçebilirsin.
58
+ </div>
59
+ ) : null}
60
+
61
+ <div className={styles.panel}>
62
+ <table className={styles.table}>
63
+ <tbody>
64
+ {rows.map((row) => (
65
+ <tr key={row.label}>
66
+ <td style={{ width: 220, color: "var(--p-muted)" }}>
67
+ {row.label}
68
+ </td>
69
+ <td>{row.value}</td>
70
+ </tr>
71
+ ))}
72
+ </tbody>
73
+ </table>
74
+ </div>
75
+ </div>
76
+ );
77
+ }