@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,19 @@
1
+ import type { JsonStore } from "../storage/json-store";
2
+ import { DEFAULT_THEME, type OcsmTheme } from "./theme.types";
3
+
4
+ const THEME_PATH = "ocsm/theme.json";
5
+
6
+ /** Reads and writes the public-site theme from `ocsm/theme.json`. */
7
+ export class ThemeStore {
8
+ constructor(private readonly json: JsonStore) {}
9
+
10
+ /** Returns the saved theme merged over defaults (so new fields always resolve). */
11
+ async get(): Promise<OcsmTheme> {
12
+ const saved = await this.json.read<Partial<OcsmTheme>>(THEME_PATH);
13
+ return { ...DEFAULT_THEME, ...(saved ?? {}) };
14
+ }
15
+
16
+ async save(theme: OcsmTheme): Promise<void> {
17
+ await this.json.write<OcsmTheme>(THEME_PATH, theme, "ocsm: update theme");
18
+ }
19
+ }
@@ -0,0 +1,53 @@
1
+ /** Visual theme for the public site, editable from the admin panel. */
2
+ export interface OcsmTheme {
3
+ primaryColor: string;
4
+ accentColor: string;
5
+ backgroundColor: string;
6
+ surfaceColor: string;
7
+ textColor: string;
8
+ mutedColor: string;
9
+ fontFamily: string;
10
+ headingFontFamily: string;
11
+ radius: string;
12
+ maxWidth: string;
13
+ }
14
+
15
+ /** A single editable theme field, used to render the theme editor generically. */
16
+ export interface ThemeFieldDescriptor {
17
+ key: keyof OcsmTheme;
18
+ label: string;
19
+ type: "color" | "text";
20
+ help?: string;
21
+ }
22
+
23
+ export const DEFAULT_THEME: OcsmTheme = {
24
+ primaryColor: "#2563eb",
25
+ accentColor: "#7c3aed",
26
+ backgroundColor: "#ffffff",
27
+ surfaceColor: "#f8fafc",
28
+ textColor: "#0f172a",
29
+ mutedColor: "#64748b",
30
+ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
31
+ headingFontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
32
+ radius: "10px",
33
+ maxWidth: "720px",
34
+ };
35
+
36
+ /** Field metadata driving the theme editor UI. */
37
+ export const THEME_FIELDS: readonly ThemeFieldDescriptor[] = [
38
+ { key: "primaryColor", label: "Birincil renk", type: "color" },
39
+ { key: "accentColor", label: "Vurgu rengi", type: "color" },
40
+ { key: "backgroundColor", label: "Arka plan", type: "color" },
41
+ { key: "surfaceColor", label: "Yüzey rengi", type: "color" },
42
+ { key: "textColor", label: "Metin rengi", type: "color" },
43
+ { key: "mutedColor", label: "Soluk metin", type: "color" },
44
+ {
45
+ key: "fontFamily",
46
+ label: "Gövde fontu",
47
+ type: "text",
48
+ help: "CSS font-family değeri",
49
+ },
50
+ { key: "headingFontFamily", label: "Başlık fontu", type: "text" },
51
+ { key: "radius", label: "Köşe yuvarlaklığı", type: "text", help: "örn. 10px" },
52
+ { key: "maxWidth", label: "İçerik genişliği", type: "text", help: "örn. 720px" },
53
+ ];
@@ -0,0 +1,4 @@
1
+ declare module "*.module.css" {
2
+ const classes: { readonly [key: string]: string };
3
+ export default classes;
4
+ }
@@ -0,0 +1,50 @@
1
+ import { OCSM_PACKAGE_NAME, OCSM_VERSION } from "../version";
2
+
3
+ export interface UpdateStatus {
4
+ /** The version currently installed. */
5
+ current: string;
6
+ /** The latest version published under the queried dist-tag, or `null` on failure. */
7
+ latest: string | null;
8
+ /** Whether a newer version is available. */
9
+ updateAvailable: boolean;
10
+ }
11
+
12
+ const REGISTRY_URL = "https://registry.npmjs.org";
13
+
14
+ /**
15
+ * Checks the npm registry for a newer release of the OCS Management core.
16
+ *
17
+ * Powers the "update available" notice in the admin panel. Network failures are
18
+ * swallowed and reported as "no update available" so the panel never breaks.
19
+ *
20
+ * @param distTag - npm dist-tag to compare against (default `"alpha"`).
21
+ */
22
+ export async function checkForUpdates(distTag = "alpha"): Promise<UpdateStatus> {
23
+ try {
24
+ const response = await fetch(`${REGISTRY_URL}/${OCSM_PACKAGE_NAME}`, {
25
+ headers: { accept: "application/vnd.npm.install-v1+json" },
26
+ // Cache for an hour to avoid hammering the registry on every render.
27
+ next: { revalidate: 3600 },
28
+ } as RequestInit);
29
+
30
+ if (!response.ok) return noUpdate();
31
+
32
+ const data = (await response.json()) as {
33
+ "dist-tags"?: Record<string, string>;
34
+ };
35
+ const latest =
36
+ data["dist-tags"]?.[distTag] ?? data["dist-tags"]?.latest ?? null;
37
+
38
+ return {
39
+ current: OCSM_VERSION,
40
+ latest,
41
+ updateAvailable: latest !== null && latest !== OCSM_VERSION,
42
+ };
43
+ } catch {
44
+ return noUpdate();
45
+ }
46
+ }
47
+
48
+ function noUpdate(): UpdateStatus {
49
+ return { current: OCSM_VERSION, latest: null, updateAvailable: false };
50
+ }
@@ -0,0 +1 @@
1
+ export { checkForUpdates, type UpdateStatus } from "./check-for-updates";
@@ -0,0 +1,6 @@
1
+ export type { OcsmUser, PublicUser, UsersFile } from "./user.types";
2
+ export {
3
+ type CreateUserInput,
4
+ type UpdateUserInput,
5
+ UserStore,
6
+ } from "./user-store";
@@ -0,0 +1,120 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { hashPassword } from "../auth/password";
3
+ import { SYSTEM_ADMIN_ROLE_ID } from "../roles/role.types";
4
+ import type { JsonStore } from "../storage/json-store";
5
+ import type { OcsmUser, PublicUser, UsersFile } from "./user.types";
6
+
7
+ const USERS_PATH = "ocsm/users.json";
8
+
9
+ export interface CreateUserInput {
10
+ username: string;
11
+ name: string;
12
+ role: string;
13
+ password: string;
14
+ }
15
+
16
+ export interface UpdateUserInput {
17
+ name: string;
18
+ role: string;
19
+ /** When provided and non-empty, the password is changed. */
20
+ password?: string;
21
+ }
22
+
23
+ /** Manages users persisted to `ocsm/users.json`. */
24
+ export class UserStore {
25
+ constructor(private readonly json: JsonStore) {}
26
+
27
+ async count(): Promise<number> {
28
+ return (await this.all()).length;
29
+ }
30
+
31
+ /** Lists users without password hashes. */
32
+ async list(): Promise<PublicUser[]> {
33
+ return (await this.all()).map(toPublicUser);
34
+ }
35
+
36
+ /** Distinct role ids currently assigned to users. */
37
+ async assignedRoleIds(): Promise<string[]> {
38
+ return [...new Set((await this.all()).map((u) => u.role))];
39
+ }
40
+
41
+ async findByUsername(username: string): Promise<OcsmUser | null> {
42
+ const normalized = username.trim().toLowerCase();
43
+ return (
44
+ (await this.all()).find((u) => u.username.toLowerCase() === normalized) ??
45
+ null
46
+ );
47
+ }
48
+
49
+ async findById(id: string): Promise<OcsmUser | null> {
50
+ return (await this.all()).find((u) => u.id === id) ?? null;
51
+ }
52
+
53
+ async create(input: CreateUserInput): Promise<PublicUser> {
54
+ const users = await this.all();
55
+ const username = input.username.trim();
56
+ if (!username) throw new Error("OCSM: kullanıcı adı boş olamaz");
57
+ if (users.some((u) => u.username.toLowerCase() === username.toLowerCase())) {
58
+ throw new Error(`OCSM: "${username}" kullanıcı adı zaten kayıtlı`);
59
+ }
60
+
61
+ const user: OcsmUser = {
62
+ id: randomUUID(),
63
+ username,
64
+ name: input.name.trim() || username,
65
+ role: input.role,
66
+ passwordHash: hashPassword(input.password),
67
+ createdAt: new Date().toISOString(),
68
+ };
69
+ await this.save([...users, user]);
70
+ return toPublicUser(user);
71
+ }
72
+
73
+ async update(id: string, input: UpdateUserInput): Promise<void> {
74
+ const users = await this.all();
75
+ const target = users.find((u) => u.id === id);
76
+ if (!target) throw new Error("OCSM: kullanıcı bulunamadı");
77
+
78
+ if (
79
+ target.role === SYSTEM_ADMIN_ROLE_ID &&
80
+ input.role !== SYSTEM_ADMIN_ROLE_ID &&
81
+ countAdmins(users) <= 1
82
+ ) {
83
+ throw new Error("OCSM: son yöneticinin rolü değiştirilemez");
84
+ }
85
+
86
+ target.name = input.name.trim() || target.name;
87
+ target.role = input.role;
88
+ if (input.password && input.password.length > 0) {
89
+ target.passwordHash = hashPassword(input.password);
90
+ }
91
+ await this.save(users);
92
+ }
93
+
94
+ async remove(id: string): Promise<void> {
95
+ const users = await this.all();
96
+ const target = users.find((u) => u.id === id);
97
+ if (!target) return;
98
+ if (target.role === SYSTEM_ADMIN_ROLE_ID && countAdmins(users) <= 1) {
99
+ throw new Error("OCSM: son yönetici silinemez");
100
+ }
101
+ await this.save(users.filter((u) => u.id !== id));
102
+ }
103
+
104
+ private async all(): Promise<OcsmUser[]> {
105
+ return (await this.json.readOr<UsersFile>(USERS_PATH, { users: [] })).users;
106
+ }
107
+
108
+ private async save(users: OcsmUser[]): Promise<void> {
109
+ await this.json.write<UsersFile>(USERS_PATH, { users }, "ocsm: update users");
110
+ }
111
+ }
112
+
113
+ function toPublicUser(user: OcsmUser): PublicUser {
114
+ const { passwordHash: _passwordHash, ...rest } = user;
115
+ return rest;
116
+ }
117
+
118
+ function countAdmins(users: OcsmUser[]): number {
119
+ return users.filter((u) => u.role === SYSTEM_ADMIN_ROLE_ID).length;
120
+ }
@@ -0,0 +1,18 @@
1
+ /** A stored user (includes the password hash — never expose to the client). */
2
+ export interface OcsmUser {
3
+ id: string;
4
+ username: string;
5
+ name: string;
6
+ /** References a role id from the role store. */
7
+ role: string;
8
+ passwordHash: string;
9
+ createdAt: string;
10
+ }
11
+
12
+ /** A user without sensitive fields, safe to render. */
13
+ export type PublicUser = Omit<OcsmUser, "passwordHash">;
14
+
15
+ /** On-disk shape of `ocsm/users.json`. */
16
+ export interface UsersFile {
17
+ users: OcsmUser[];
18
+ }
package/src/version.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The installed OCS Management core version.
3
+ *
4
+ * Kept in sync with this package's `package.json` `version` field. Consumed by the
5
+ * update checker ({@link ./update/check-for-updates}) and the admin "update available"
6
+ * notice.
7
+ */
8
+ export const OCSM_VERSION = "0.1.0-alpha.1";
9
+
10
+ /** The published npm package name for the OCS Management core. */
11
+ export const OCSM_PACKAGE_NAME = "@orhancodestudio/ocsm-core";