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.
- package/CHANGELOG.md +52 -0
- package/ROADMAP.md +17 -15
- package/dist/admin/auth-client.d.ts +1165 -5
- package/dist/admin/auth-client.js +4 -1
- package/dist/admin/client/account/sessions-section.svelte +1 -21
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/users/accept-invite-page.svelte +118 -0
- package/dist/admin/client/users/accept-invite-page.svelte.d.ts +4 -0
- package/dist/admin/client/users/create-user-dialog.svelte +157 -0
- package/dist/admin/client/users/create-user-dialog.svelte.d.ts +8 -0
- package/dist/admin/client/users/delete-user-dialog.svelte +53 -0
- package/dist/admin/client/users/delete-user-dialog.svelte.d.ts +10 -0
- package/dist/admin/client/users/edit-user-dialog.svelte +127 -0
- package/dist/admin/client/users/edit-user-dialog.svelte.d.ts +16 -0
- package/dist/admin/client/users/invite-user-dialog.svelte +107 -0
- package/dist/admin/client/users/invite-user-dialog.svelte.d.ts +8 -0
- package/dist/admin/client/users/lang.d.ts +57 -0
- package/dist/admin/client/users/lang.js +114 -0
- package/dist/admin/client/users/pending-invitations.svelte +145 -0
- package/dist/admin/client/users/pending-invitations.svelte.d.ts +6 -0
- package/dist/admin/client/users/user-sessions-sheet.svelte +141 -0
- package/dist/admin/client/users/user-sessions-sheet.svelte.d.ts +8 -0
- package/dist/admin/client/users/users-page.svelte +262 -0
- package/dist/admin/client/users/users-page.svelte.d.ts +6 -0
- package/dist/admin/components/fields/array-field.svelte +68 -22
- package/dist/admin/components/fields/field-renderer.svelte +25 -2
- package/dist/admin/components/fields/number-field.svelte +1 -1
- package/dist/admin/components/fields/text-field-wrapper.svelte +56 -1
- package/dist/admin/components/fields/text-field.svelte +2 -2
- package/dist/admin/components/layout/lang.d.ts +1 -0
- package/dist/admin/components/layout/lang.js +4 -2
- package/dist/admin/components/layout/nav-main.svelte +15 -1
- package/dist/admin/remote/invite.d.ts +44 -0
- package/dist/admin/remote/invite.js +44 -0
- package/dist/admin/remote/middleware/auth.d.ts +5 -0
- package/dist/admin/remote/middleware/auth.js +7 -0
- package/dist/admin/utils/parseUserAgent.d.ts +5 -0
- package/dist/admin/utils/parseUserAgent.js +26 -0
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/core/cms.d.ts +1 -1
- package/dist/core/cms.js +1 -1
- package/dist/core/fields/fieldSchemaToTs.js +18 -4
- package/dist/core/server/forms/submissions/operations/create.js +1 -1
- package/dist/email-nodemailer/index.d.ts +1 -0
- package/dist/server/auth.d.ts +8 -8
- package/dist/server/db/schema/auth-schema.d.ts +143 -0
- package/dist/server/db/schema/auth-schema.js +12 -0
- package/dist/sveltekit/server/handle.js +13 -0
- package/dist/types/cms.d.ts +2 -2
- package/dist/types/roles.d.ts +1 -0
- package/dist/types/roles.js +1 -0
- package/dist/updates/0.1.1/index.d.ts +2 -0
- package/dist/updates/0.1.1/index.js +17 -0
- package/dist/updates/0.1.2/index.d.ts +2 -0
- package/dist/updates/0.1.2/index.js +36 -0
- package/dist/updates/index.js +3 -1
- 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,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 & {}) | "
|
|
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 & {}) | "
|
|
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;
|
package/dist/core/cms.d.ts
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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')}"`,
|
package/dist/server/auth.d.ts
CHANGED
|
@@ -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?: ("
|
|
1171
|
-
readonly session?: ("
|
|
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?: ("
|
|
1177
|
-
readonly session?: ("
|
|
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?: ("
|
|
1274
|
-
readonly session?: ("
|
|
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?: ("
|
|
1280
|
-
readonly session?: ("
|
|
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
|
package/dist/types/cms.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface CMSConfig {
|
|
|
18
18
|
forms?: FormConfig[];
|
|
19
19
|
db: DatabaseAdapter;
|
|
20
20
|
files: FilesAdapter;
|
|
21
|
-
email
|
|
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,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,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
|
+
};
|
package/dist/updates/index.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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",
|