includio-cms 0.1.0 → 0.1.2

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 (59) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/ROADMAP.md +17 -15
  3. package/dist/admin/auth-client.d.ts +1165 -5
  4. package/dist/admin/auth-client.js +4 -1
  5. package/dist/admin/client/account/sessions-section.svelte +1 -21
  6. package/dist/admin/client/index.d.ts +1 -0
  7. package/dist/admin/client/index.js +1 -0
  8. package/dist/admin/client/users/accept-invite-page.svelte +118 -0
  9. package/dist/admin/client/users/accept-invite-page.svelte.d.ts +4 -0
  10. package/dist/admin/client/users/create-user-dialog.svelte +157 -0
  11. package/dist/admin/client/users/create-user-dialog.svelte.d.ts +8 -0
  12. package/dist/admin/client/users/delete-user-dialog.svelte +53 -0
  13. package/dist/admin/client/users/delete-user-dialog.svelte.d.ts +10 -0
  14. package/dist/admin/client/users/edit-user-dialog.svelte +127 -0
  15. package/dist/admin/client/users/edit-user-dialog.svelte.d.ts +16 -0
  16. package/dist/admin/client/users/invite-user-dialog.svelte +107 -0
  17. package/dist/admin/client/users/invite-user-dialog.svelte.d.ts +8 -0
  18. package/dist/admin/client/users/lang.d.ts +57 -0
  19. package/dist/admin/client/users/lang.js +114 -0
  20. package/dist/admin/client/users/pending-invitations.svelte +145 -0
  21. package/dist/admin/client/users/pending-invitations.svelte.d.ts +6 -0
  22. package/dist/admin/client/users/user-sessions-sheet.svelte +141 -0
  23. package/dist/admin/client/users/user-sessions-sheet.svelte.d.ts +8 -0
  24. package/dist/admin/client/users/users-page.svelte +262 -0
  25. package/dist/admin/client/users/users-page.svelte.d.ts +6 -0
  26. package/dist/admin/components/fields/array-field.svelte +68 -22
  27. package/dist/admin/components/fields/field-renderer.svelte +25 -2
  28. package/dist/admin/components/fields/number-field.svelte +1 -1
  29. package/dist/admin/components/fields/text-field-wrapper.svelte +56 -1
  30. package/dist/admin/components/fields/text-field.svelte +2 -2
  31. package/dist/admin/components/layout/lang.d.ts +1 -0
  32. package/dist/admin/components/layout/lang.js +4 -2
  33. package/dist/admin/components/layout/nav-main.svelte +15 -1
  34. package/dist/admin/remote/invite.d.ts +44 -0
  35. package/dist/admin/remote/invite.js +44 -0
  36. package/dist/admin/remote/middleware/auth.d.ts +5 -0
  37. package/dist/admin/remote/middleware/auth.js +7 -0
  38. package/dist/admin/utils/parseUserAgent.d.ts +5 -0
  39. package/dist/admin/utils/parseUserAgent.js +26 -0
  40. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  41. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  42. package/dist/core/cms.d.ts +1 -1
  43. package/dist/core/cms.js +1 -1
  44. package/dist/core/fields/fieldSchemaToTs.js +18 -4
  45. package/dist/core/server/forms/submissions/operations/create.js +1 -1
  46. package/dist/email-nodemailer/index.d.ts +1 -0
  47. package/dist/server/auth.d.ts +8 -8
  48. package/dist/server/db/schema/auth-schema.d.ts +143 -0
  49. package/dist/server/db/schema/auth-schema.js +12 -0
  50. package/dist/sveltekit/server/handle.js +13 -0
  51. package/dist/types/cms.d.ts +2 -2
  52. package/dist/types/roles.d.ts +1 -0
  53. package/dist/types/roles.js +1 -0
  54. package/dist/updates/0.1.1/index.d.ts +2 -0
  55. package/dist/updates/0.1.1/index.js +17 -0
  56. package/dist/updates/0.1.2/index.d.ts +2 -0
  57. package/dist/updates/0.1.2/index.js +36 -0
  58. package/dist/updates/index.js +3 -1
  59. package/package.json +2 -2
@@ -0,0 +1,44 @@
1
+ import { db } from '../../server/db/index.js';
2
+ import { invitation, user } from '../../server/db/schema/auth-schema.js';
3
+ import { eq, and, isNull, gt } from 'drizzle-orm';
4
+ export async function createInvitation(email, role, createdBy) {
5
+ const token = crypto.randomUUID();
6
+ const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
7
+ const id = crypto.randomUUID();
8
+ const [row] = await db
9
+ .insert(invitation)
10
+ .values({ id, email, role, token, expiresAt, createdBy })
11
+ .returning();
12
+ return row;
13
+ }
14
+ export async function getInvitationByToken(token) {
15
+ const [row] = await db
16
+ .select()
17
+ .from(invitation)
18
+ .where(and(eq(invitation.token, token), isNull(invitation.usedAt), gt(invitation.expiresAt, new Date())));
19
+ return row ?? null;
20
+ }
21
+ export async function markInvitationUsed(id) {
22
+ await db.update(invitation).set({ usedAt: new Date() }).where(eq(invitation.id, id));
23
+ }
24
+ export async function getPendingInvitations() {
25
+ return db
26
+ .select()
27
+ .from(invitation)
28
+ .where(and(isNull(invitation.usedAt), gt(invitation.expiresAt, new Date())))
29
+ .orderBy(invitation.createdAt);
30
+ }
31
+ export async function getInvitationById(id) {
32
+ const [row] = await db
33
+ .select()
34
+ .from(invitation)
35
+ .where(and(eq(invitation.id, id), isNull(invitation.usedAt), gt(invitation.expiresAt, new Date())));
36
+ return row ?? null;
37
+ }
38
+ export async function deleteInvitation(id) {
39
+ await db.delete(invitation).where(eq(invitation.id, id));
40
+ }
41
+ export async function checkEmailExists(email) {
42
+ const [row] = await db.select({ id: user.id }).from(user).where(eq(user.email, email)).limit(1);
43
+ return !!row;
44
+ }
@@ -1,5 +1,10 @@
1
1
  import type { Session, User } from 'better-auth';
2
+ import type { UserRole } from '../../../types/roles.js';
2
3
  export declare function requireAuth(): {
3
4
  user: User;
4
5
  session: Session;
5
6
  };
7
+ export declare function requireRole(...roles: UserRole[]): {
8
+ user: User;
9
+ session: Session;
10
+ };
@@ -7,3 +7,10 @@ export function requireAuth() {
7
7
  }
8
8
  return { user: authLocals.user, session: authLocals.session };
9
9
  }
10
+ export function requireRole(...roles) {
11
+ const { user, session } = requireAuth();
12
+ if (!roles.includes(user.role)) {
13
+ throw new Error('Forbidden');
14
+ }
15
+ return { user, session };
16
+ }
@@ -0,0 +1,5 @@
1
+ export declare function parseUserAgent(ua: string | null | undefined): {
2
+ browser: string;
3
+ os: string;
4
+ isMobile: boolean;
5
+ };
@@ -0,0 +1,26 @@
1
+ export function parseUserAgent(ua) {
2
+ if (!ua)
3
+ return { browser: 'Unknown', os: 'Unknown', isMobile: false };
4
+ const isMobile = /mobile|android|iphone|ipad/i.test(ua);
5
+ let browser = 'Unknown';
6
+ if (ua.includes('Firefox'))
7
+ browser = 'Firefox';
8
+ else if (ua.includes('Edg'))
9
+ browser = 'Edge';
10
+ else if (ua.includes('Chrome'))
11
+ browser = 'Chrome';
12
+ else if (ua.includes('Safari'))
13
+ browser = 'Safari';
14
+ let os = 'Unknown';
15
+ if (ua.includes('iPhone') || ua.includes('iPad'))
16
+ os = 'iOS';
17
+ else if (ua.includes('Android'))
18
+ os = 'Android';
19
+ else if (ua.includes('Windows'))
20
+ os = 'Windows';
21
+ else if (ua.includes('Mac'))
22
+ os = 'macOS';
23
+ else if (ua.includes('Linux'))
24
+ os = 'Linux';
25
+ return { browser, os, isMobile };
26
+ }
@@ -2,7 +2,7 @@ declare const InputGroupInput: import("svelte").Component<(Omit<import("svelte/e
2
2
  type: "file";
3
3
  files?: FileList;
4
4
  } | {
5
- type?: "number" | "image" | "url" | "text" | "date" | "radio" | "email" | "checkbox" | "hidden" | (string & {}) | "password" | "reset" | "search" | "time" | "color" | "button" | "submit" | "tel" | "datetime-local" | "month" | "range" | "week";
5
+ type?: "number" | "image" | "url" | "text" | "date" | "radio" | "email" | "checkbox" | "hidden" | "password" | (string & {}) | "reset" | "search" | "time" | "color" | "button" | "submit" | "tel" | "datetime-local" | "month" | "range" | "week";
6
6
  files?: undefined;
7
7
  })) & {
8
8
  ref?: HTMLElement | null | undefined;
@@ -2,7 +2,7 @@ declare const SidebarInput: import("svelte").Component<(Omit<import("svelte/elem
2
2
  type: "file";
3
3
  files?: FileList;
4
4
  } | {
5
- type?: "number" | "image" | "url" | "text" | "date" | "radio" | "email" | "checkbox" | "hidden" | (string & {}) | "password" | "reset" | "search" | "time" | "color" | "button" | "submit" | "tel" | "datetime-local" | "month" | "range" | "week";
5
+ type?: "number" | "image" | "url" | "text" | "date" | "radio" | "email" | "checkbox" | "hidden" | "password" | (string & {}) | "reset" | "search" | "time" | "color" | "button" | "submit" | "tel" | "datetime-local" | "month" | "range" | "week";
6
6
  files?: undefined;
7
7
  })) & {
8
8
  ref?: HTMLElement | null | undefined;
@@ -12,7 +12,7 @@ export declare class CMS implements ICMS {
12
12
  private config;
13
13
  databaseAdapter: DatabaseAdapter;
14
14
  filesAdapter: FilesAdapter;
15
- emailAdapter: EmailAdapter;
15
+ emailAdapter: EmailAdapter | null;
16
16
  aiAdapter: AIAdapter | null;
17
17
  collections: Record<string, CollectionConfigWithType>;
18
18
  singles: Record<string, SingleConfigWithType>;
package/dist/core/cms.js CHANGED
@@ -14,7 +14,7 @@ export class CMS {
14
14
  this.config = config;
15
15
  this.databaseAdapter = config.db;
16
16
  this.filesAdapter = config.files;
17
- this.emailAdapter = config.email;
17
+ this.emailAdapter = config.email ?? null;
18
18
  this.aiAdapter = config.ai || null;
19
19
  this.mediaConfig = config.media || {};
20
20
  this.collections = {};
@@ -74,10 +74,24 @@ export function generateZodSchemaFromField(field, languages, options = {
74
74
  ? z.discriminatedUnion('slug', schemas)
75
75
  : schemas[0];
76
76
  let schema = z.array(itemSchema);
77
- if (field.minItems !== undefined)
78
- schema = schema.min(field.minItems, { message: `Minimum ${field.minItems} elementów` });
79
- if (field.maxItems !== undefined)
80
- schema = schema.max(field.maxItems, { message: `Maksimum ${field.maxItems} elementów` });
77
+ if (field.minItems !== undefined &&
78
+ field.maxItems !== undefined &&
79
+ field.minItems === field.maxItems &&
80
+ field.minItems > 0) {
81
+ schema = schema.length(field.minItems, {
82
+ message: `Wymagane dokładnie ${field.minItems} elementów`
83
+ });
84
+ }
85
+ else {
86
+ if (field.minItems !== undefined)
87
+ schema = schema.min(field.minItems, {
88
+ message: `Minimum ${field.minItems} elementów`
89
+ });
90
+ if (field.maxItems !== undefined)
91
+ schema = schema.max(field.maxItems, {
92
+ message: `Maksimum ${field.maxItems} elementów`
93
+ });
94
+ }
81
95
  return schema.default([]); // Default to empty array if not required
82
96
  }
83
97
  case 'slug': {
@@ -16,7 +16,7 @@ export const createFormSubmission = async (options) => {
16
16
  ip,
17
17
  userAgent
18
18
  });
19
- if (config.notificationEmailAddresses && config.notificationEmailAddresses.length > 0) {
19
+ if (config.notificationEmailAddresses && config.notificationEmailAddresses.length > 0 && getCMS().emailAdapter) {
20
20
  await getCMS().emailAdapter.sendMail({
21
21
  to: config.notificationEmailAddresses,
22
22
  subject: `New submission for form "${getLocalizedLabel(config.label, 'en')}"`,
@@ -5,6 +5,7 @@ interface Options {
5
5
  transportOptions: {
6
6
  host: string;
7
7
  port: number;
8
+ secure?: boolean;
8
9
  auth: {
9
10
  user: string;
10
11
  pass: string;
@@ -1167,14 +1167,14 @@ export declare const auth: import("better-auth", { with: { "resolution-mode": "r
1167
1167
  <AsResponse extends boolean = false, ReturnHeaders extends boolean = false>(inputCtx_0: {
1168
1168
  body: ({
1169
1169
  permission: {
1170
- readonly user?: ("update" | "delete" | "list" | "get" | "create" | "set-role" | "ban" | "impersonate" | "set-password")[] | undefined;
1171
- readonly session?: ("delete" | "list" | "revoke")[] | undefined;
1170
+ readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1171
+ readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1172
1172
  };
1173
1173
  permissions?: never;
1174
1174
  } | {
1175
1175
  permissions: {
1176
- readonly user?: ("update" | "delete" | "list" | "get" | "create" | "set-role" | "ban" | "impersonate" | "set-password")[] | undefined;
1177
- readonly session?: ("delete" | "list" | "revoke")[] | undefined;
1176
+ readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1177
+ readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1178
1178
  };
1179
1179
  permission?: never;
1180
1180
  }) & {
@@ -1270,14 +1270,14 @@ export declare const auth: import("better-auth", { with: { "resolution-mode": "r
1270
1270
  $Infer: {
1271
1271
  body: ({
1272
1272
  permission: {
1273
- readonly user?: ("update" | "delete" | "list" | "get" | "create" | "set-role" | "ban" | "impersonate" | "set-password")[] | undefined;
1274
- readonly session?: ("delete" | "list" | "revoke")[] | undefined;
1273
+ readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1274
+ readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1275
1275
  };
1276
1276
  permissions?: never;
1277
1277
  } | {
1278
1278
  permissions: {
1279
- readonly user?: ("update" | "delete" | "list" | "get" | "create" | "set-role" | "ban" | "impersonate" | "set-password")[] | undefined;
1280
- readonly session?: ("delete" | "list" | "revoke")[] | undefined;
1279
+ readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
1280
+ readonly session?: ("list" | "delete" | "revoke")[] | undefined;
1281
1281
  };
1282
1282
  permission?: never;
1283
1283
  }) & {
@@ -689,6 +689,149 @@ export declare const verification: import("drizzle-orm/pg-core/table", { with: {
689
689
  };
690
690
  dialect: "pg";
691
691
  }>;
692
+ export declare const invitation: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
693
+ name: "invitation";
694
+ schema: undefined;
695
+ columns: {
696
+ id: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
697
+ name: "id";
698
+ tableName: "invitation";
699
+ dataType: "string";
700
+ columnType: "PgText";
701
+ data: string;
702
+ driverParam: string;
703
+ notNull: true;
704
+ hasDefault: false;
705
+ isPrimaryKey: true;
706
+ isAutoincrement: false;
707
+ hasRuntimeDefault: false;
708
+ enumValues: [string, ...string[]];
709
+ baseColumn: never;
710
+ identity: undefined;
711
+ generated: undefined;
712
+ }, {}, {}>;
713
+ email: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
714
+ name: "email";
715
+ tableName: "invitation";
716
+ dataType: "string";
717
+ columnType: "PgText";
718
+ data: string;
719
+ driverParam: string;
720
+ notNull: true;
721
+ hasDefault: false;
722
+ isPrimaryKey: false;
723
+ isAutoincrement: false;
724
+ hasRuntimeDefault: false;
725
+ enumValues: [string, ...string[]];
726
+ baseColumn: never;
727
+ identity: undefined;
728
+ generated: undefined;
729
+ }, {}, {}>;
730
+ role: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
731
+ name: "role";
732
+ tableName: "invitation";
733
+ dataType: "string";
734
+ columnType: "PgText";
735
+ data: string;
736
+ driverParam: string;
737
+ notNull: true;
738
+ hasDefault: true;
739
+ isPrimaryKey: false;
740
+ isAutoincrement: false;
741
+ hasRuntimeDefault: false;
742
+ enumValues: [string, ...string[]];
743
+ baseColumn: never;
744
+ identity: undefined;
745
+ generated: undefined;
746
+ }, {}, {}>;
747
+ token: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
748
+ name: "token";
749
+ tableName: "invitation";
750
+ dataType: "string";
751
+ columnType: "PgText";
752
+ data: string;
753
+ driverParam: string;
754
+ notNull: true;
755
+ hasDefault: false;
756
+ isPrimaryKey: false;
757
+ isAutoincrement: false;
758
+ hasRuntimeDefault: false;
759
+ enumValues: [string, ...string[]];
760
+ baseColumn: never;
761
+ identity: undefined;
762
+ generated: undefined;
763
+ }, {}, {}>;
764
+ expiresAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
765
+ name: "expires_at";
766
+ tableName: "invitation";
767
+ dataType: "date";
768
+ columnType: "PgTimestamp";
769
+ data: Date;
770
+ driverParam: string;
771
+ notNull: true;
772
+ hasDefault: false;
773
+ isPrimaryKey: false;
774
+ isAutoincrement: false;
775
+ hasRuntimeDefault: false;
776
+ enumValues: undefined;
777
+ baseColumn: never;
778
+ identity: undefined;
779
+ generated: undefined;
780
+ }, {}, {}>;
781
+ createdAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
782
+ name: "created_at";
783
+ tableName: "invitation";
784
+ dataType: "date";
785
+ columnType: "PgTimestamp";
786
+ data: Date;
787
+ driverParam: string;
788
+ notNull: true;
789
+ hasDefault: true;
790
+ isPrimaryKey: false;
791
+ isAutoincrement: false;
792
+ hasRuntimeDefault: false;
793
+ enumValues: undefined;
794
+ baseColumn: never;
795
+ identity: undefined;
796
+ generated: undefined;
797
+ }, {}, {}>;
798
+ createdBy: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
799
+ name: "created_by";
800
+ tableName: "invitation";
801
+ dataType: "string";
802
+ columnType: "PgText";
803
+ data: string;
804
+ driverParam: string;
805
+ notNull: true;
806
+ hasDefault: false;
807
+ isPrimaryKey: false;
808
+ isAutoincrement: false;
809
+ hasRuntimeDefault: false;
810
+ enumValues: [string, ...string[]];
811
+ baseColumn: never;
812
+ identity: undefined;
813
+ generated: undefined;
814
+ }, {}, {}>;
815
+ usedAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
816
+ name: "used_at";
817
+ tableName: "invitation";
818
+ dataType: "date";
819
+ columnType: "PgTimestamp";
820
+ data: Date;
821
+ driverParam: string;
822
+ notNull: false;
823
+ hasDefault: false;
824
+ isPrimaryKey: false;
825
+ isAutoincrement: false;
826
+ hasRuntimeDefault: false;
827
+ enumValues: undefined;
828
+ baseColumn: never;
829
+ identity: undefined;
830
+ generated: undefined;
831
+ }, {}, {}>;
832
+ };
833
+ dialect: "pg";
834
+ }>;
692
835
  export declare const userRelations: import("drizzle-orm/relations", { with: { "resolution-mode": "require" } }).Relations<"user", {
693
836
  sessions: import("drizzle-orm/relations", { with: { "resolution-mode": "require" } }).Many<"session">;
694
837
  accounts: import("drizzle-orm/relations", { with: { "resolution-mode": "require" } }).Many<"account">;
@@ -61,6 +61,18 @@ export const verification = pgTable('verification', {
61
61
  .$onUpdate(() => /* @__PURE__ */ new Date())
62
62
  .notNull()
63
63
  }, (table) => [index('verification_identifier_idx').on(table.identifier)]);
64
+ export const invitation = pgTable('invitation', {
65
+ id: text('id').primaryKey(),
66
+ email: text('email').notNull(),
67
+ role: text('role').notNull().default('user'),
68
+ token: text('token').notNull().unique(),
69
+ expiresAt: timestamp('expires_at').notNull(),
70
+ createdAt: timestamp('created_at').defaultNow().notNull(),
71
+ createdBy: text('created_by')
72
+ .notNull()
73
+ .references(() => user.id, { onDelete: 'cascade' }),
74
+ usedAt: timestamp('used_at')
75
+ }, (table) => [index('invitation_token_idx').on(table.token)]);
64
76
  export const userRelations = relations(user, ({ many }) => ({
65
77
  sessions: many(session),
66
78
  accounts: many(account)
@@ -8,6 +8,8 @@ const adminGuard = async ({ event, resolve }) => {
8
8
  if (event.url.pathname.startsWith('/admin') || event.url.pathname.startsWith('/api/admin')) {
9
9
  if (!event.url.pathname.startsWith('/admin/login') &&
10
10
  !event.url.pathname.startsWith('/api/admin/login') &&
11
+ !event.url.pathname.startsWith('/admin/accept-invite') &&
12
+ !event.url.pathname.startsWith('/admin/api/accept-invite') &&
11
13
  (!user || !session)) {
12
14
  setFlash({
13
15
  message: 'You must be logged in to access the admin panel.',
@@ -18,6 +20,17 @@ const adminGuard = async ({ event, resolve }) => {
18
20
  headers: { location: '/admin/login' }
19
21
  });
20
22
  }
23
+ // Admin-only routes
24
+ if (event.url.pathname.startsWith('/admin/users') && user && user.role !== 'admin') {
25
+ setFlash({
26
+ message: 'Insufficient permissions.',
27
+ type: 'error'
28
+ }, event);
29
+ return new Response(null, {
30
+ status: 303,
31
+ headers: { location: '/admin' }
32
+ });
33
+ }
21
34
  }
22
35
  if (event.url.pathname === '/admin/login' && user && session) {
23
36
  // If the user is already logged in, redirect to the admin dashboard
@@ -18,7 +18,7 @@ export interface CMSConfig {
18
18
  forms?: FormConfig[];
19
19
  db: DatabaseAdapter;
20
20
  files: FilesAdapter;
21
- email: EmailAdapter;
21
+ email?: EmailAdapter;
22
22
  plugins?: PluginConfig[];
23
23
  ai?: AIAdapter;
24
24
  media?: MediaConfig;
@@ -30,7 +30,7 @@ export interface ICMS {
30
30
  languages: Language[];
31
31
  databaseAdapter: DatabaseAdapter;
32
32
  filesAdapter: FilesAdapter;
33
- emailAdapter: EmailAdapter;
33
+ emailAdapter: EmailAdapter | null;
34
34
  plugins: PluginConfig[];
35
35
  aiAdapter: AIAdapter | null;
36
36
  mediaConfig: MediaConfig;
@@ -0,0 +1 @@
1
+ export type UserRole = 'admin' | 'user';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,17 @@
1
+ export const update = {
2
+ version: '0.1.1',
3
+ date: '2026-02-18',
4
+ description: 'Field constraint UI — visible limits, counters, and hints',
5
+ features: [
6
+ 'Text fields: character counter (X / Y) with aria-live, destructive color at limit',
7
+ 'Text fields: constraint hints (min/max chars, pattern format) in description',
8
+ 'Text fields: native minlength/maxlength HTML attributes on input/textarea',
9
+ 'Number fields: native min/max/step HTML attributes on input',
10
+ 'Number fields: range and step hints in description',
11
+ 'Array fields: items counter (X / Y) next to label when maxItems defined',
12
+ 'Array fields: Add/Duplicate buttons disabled at maxItems limit',
13
+ 'Array fields: fixed-length mode (minItems === maxItems) — pre-populated, reorder only'
14
+ ],
15
+ fixes: [],
16
+ breakingChanges: []
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,36 @@
1
+ export const update = {
2
+ version: '0.1.2',
3
+ date: '2026-02-18',
4
+ description: 'User management & RBAC',
5
+ features: [
6
+ 'Role type system (admin/user) with UserRole type',
7
+ 'RBAC middleware: requireRole() for server-side role checks',
8
+ 'Admin users page: list, search, pagination via authClient.admin.listUsers',
9
+ 'Create user dialog: email, password, name, role via authClient.admin.createUser',
10
+ 'Edit user dialog: name, email, role with self-demotion protection',
11
+ 'Delete user dialog with self-deletion protection',
12
+ 'Route gating: /admin/users restricted to admin role',
13
+ 'Sidebar gating: Users nav item visible only for admins',
14
+ 'Auth client: adminClient() plugin added',
15
+ 'CLI: addUser rewritten to use better-auth API with role prompt',
16
+ 'Admin session management: view/revoke other users\' sessions',
17
+ 'Email invitation system: invite users by email with role assignment',
18
+ 'Accept invite page: public registration via invite token',
19
+ 'Pending invitations list with cancel and resend support',
20
+ 'Email adapter now optional in CMS config'
21
+ ],
22
+ fixes: [],
23
+ breakingChanges: [],
24
+ migration: `CREATE TABLE IF NOT EXISTS invitation (
25
+ id TEXT PRIMARY KEY,
26
+ email TEXT NOT NULL,
27
+ role TEXT NOT NULL DEFAULT 'user',
28
+ token TEXT NOT NULL UNIQUE,
29
+ expires_at TIMESTAMP NOT NULL,
30
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
31
+ created_by TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
32
+ used_at TIMESTAMP
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS invitation_token_idx ON invitation (token);`
36
+ };
@@ -4,7 +4,9 @@ import { update as update0067 } from './0.0.67/index.js';
4
4
  import { update as update0068 } from './0.0.68/index.js';
5
5
  import { update as update0069 } from './0.0.69/index.js';
6
6
  import { update as update010 } from './0.1.0/index.js';
7
- export const updates = [update0065, update0066, update0067, update0068, update0069, update010];
7
+ import { update as update011 } from './0.1.1/index.js';
8
+ import { update as update012 } from './0.1.2/index.js';
9
+ export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012];
8
10
  export const getUpdatesFrom = (fromVersion) => {
9
11
  const fromParts = fromVersion.split('.').map(Number);
10
12
  return updates.filter((update) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -20,7 +20,7 @@
20
20
  "db:studio": "drizzle-kit studio",
21
21
  "storybook": "storybook dev -p 6006",
22
22
  "build-storybook": "storybook build",
23
- "create:user": "tsx src/lib/core/server/auth/scripts/createUser.ts",
23
+ "create:user": "tsx cli/addUser/index.ts",
24
24
  "cli": "tsx cli/init/index.ts",
25
25
  "build:cli": "tsc cli/init/index.ts --outDir dist/cli/init --module commonjs --esModuleInterop",
26
26
  "changelog": "tsx scripts/generate-changelog.ts",