@nixxie-cms/core 1.0.3 → 1.1.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/CHANGES-1.1.md +134 -0
- package/context/dist/nixxie-cms-core-context.cjs.js +4 -3
- package/context/dist/nixxie-cms-core-context.esm.js +3 -2
- package/dist/declarations/src/access.d.ts +2 -2
- package/dist/declarations/src/access.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/components/Navigation.d.ts +2 -2
- package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/context.d.ts +6 -6
- package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/Fields.d.ts +3 -3
- package/dist/declarations/src/admin-ui/utils/Fields.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/filters.d.ts +5 -5
- package/dist/declarations/src/admin-ui/utils/filters.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts +3 -3
- package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
- package/dist/declarations/src/admin-ui/utils/utils.d.ts +2 -2
- package/dist/declarations/src/admin-ui/utils/utils.d.ts.map +1 -1
- package/dist/declarations/src/context.d.ts +1 -1
- package/dist/declarations/src/context.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/bigInt/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/bigInt/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/bytes/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/bytes/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/calendarDay/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/calendarDay/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/checkbox/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/checkbox/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/decimal/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/decimal/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/file/index.d.ts +4 -4
- package/dist/declarations/src/fields/types/file/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/float/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/float/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/image/index.d.ts +4 -4
- package/dist/declarations/src/fields/types/image/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/integer/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/integer/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/json/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/json/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/multiselect/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/multiselect/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/password/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/password/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/index.d.ts +8 -8
- package/dist/declarations/src/fields/types/relationship/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/ComboboxMany.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/ComboboxSingle.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/relationship/views/types.d.ts +3 -3
- package/dist/declarations/src/fields/types/relationship/views/types.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/select/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/select/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/text/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/text/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/timestamp/index.d.ts +3 -3
- package/dist/declarations/src/fields/types/timestamp/index.d.ts.map +1 -1
- package/dist/declarations/src/fields/types/virtual/index.d.ts +7 -7
- package/dist/declarations/src/fields/types/virtual/index.d.ts.map +1 -1
- package/dist/declarations/src/helpers.d.ts +249 -13
- package/dist/declarations/src/helpers.d.ts.map +1 -1
- package/dist/declarations/src/index.d.ts +9 -4
- package/dist/declarations/src/index.d.ts.map +1 -1
- package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -1
- package/dist/declarations/src/lib/admin-meta.d.ts +11 -11
- package/dist/declarations/src/lib/admin-meta.d.ts.map +1 -1
- package/dist/declarations/src/lib/core/access-control.d.ts +18 -18
- package/dist/declarations/src/lib/core/access-control.d.ts.map +1 -1
- package/dist/declarations/src/lib/core/cascade.d.ts +47 -0
- package/dist/declarations/src/lib/core/cascade.d.ts.map +1 -0
- package/dist/declarations/src/lib/core/initialise-lists.d.ts +27 -24
- package/dist/declarations/src/lib/core/initialise-lists.d.ts.map +1 -1
- package/dist/declarations/src/lib/env.d.ts +9 -0
- package/dist/declarations/src/lib/env.d.ts.map +1 -0
- package/dist/declarations/src/lib/system.d.ts +1 -1
- package/dist/declarations/src/lib/system.d.ts.map +1 -1
- package/dist/declarations/src/list-features.d.ts +162 -0
- package/dist/declarations/src/list-features.d.ts.map +1 -0
- package/dist/declarations/src/schema.d.ts +24 -23
- package/dist/declarations/src/schema.d.ts.map +1 -1
- package/dist/declarations/src/session.d.ts +75 -0
- package/dist/declarations/src/session.d.ts.map +1 -1
- package/dist/declarations/src/types/admin-meta.d.ts +11 -11
- package/dist/declarations/src/types/admin-meta.d.ts.map +1 -1
- package/dist/declarations/src/types/config/access-control.d.ts +42 -42
- package/dist/declarations/src/types/config/access-control.d.ts.map +1 -1
- package/dist/declarations/src/types/config/fields.d.ts +19 -19
- package/dist/declarations/src/types/config/fields.d.ts.map +1 -1
- package/dist/declarations/src/types/config/hooks.d.ts +131 -131
- package/dist/declarations/src/types/config/hooks.d.ts.map +1 -1
- package/dist/declarations/src/types/config/index.d.ts +171 -8
- package/dist/declarations/src/types/config/index.d.ts.map +1 -1
- package/dist/declarations/src/types/config/lists.d.ts +146 -108
- package/dist/declarations/src/types/config/lists.d.ts.map +1 -1
- package/dist/declarations/src/types/context.d.ts +349 -47
- package/dist/declarations/src/types/context.d.ts.map +1 -1
- package/dist/declarations/src/types/next-fields.d.ts +28 -28
- package/dist/declarations/src/types/next-fields.d.ts.map +1 -1
- package/dist/declarations/src/types/type-info.d.ts +3 -3
- package/dist/declarations/src/types/type-info.d.ts.map +1 -1
- package/dist/{express-7559ca2d.esm.js → express-0abbce07.esm.js} +6 -6
- package/dist/{express-455ae20c.cjs.js → express-7ca6f76a.cjs.js} +6 -6
- package/dist/{index-15c8f81e.esm.js → index-5d8b0b4e.esm.js} +363 -183
- package/dist/index-6055753b.cjs.js +393 -0
- package/dist/{index-42045902.cjs.js → index-ac29f382.cjs.js} +363 -185
- package/dist/index-f1703b7b.esm.js +386 -0
- package/dist/nixxie-cms-core.cjs.js +1387 -30
- package/dist/nixxie-cms-core.esm.js +1361 -24
- package/dist/{non-null-graphql-add6bb3d.cjs.js → non-null-graphql-4a44c122.cjs.js} +1 -1
- package/dist/{non-null-graphql-a84ed64d.esm.js → non-null-graphql-8c5feaae.esm.js} +1 -1
- package/dist/{resolve-hooks-165a9ce2.cjs.js → resolve-hooks-10a5f84c.cjs.js} +240 -6
- package/dist/{resolve-hooks-6813a045.esm.js → resolve-hooks-9e676794.esm.js} +238 -7
- package/dist/{system-03e49e4f.esm.js → system-4d2a2648.esm.js} +32 -7
- package/dist/{system-a321642d.cjs.js → system-69e1a285.cjs.js} +32 -7
- package/fields/dist/nixxie-cms-core-fields.cjs.js +29 -576
- package/fields/dist/nixxie-cms-core-fields.esm.js +18 -565
- package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +4 -2
- package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +4 -2
- package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +1 -6
- package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +1 -6
- package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +4 -2
- package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +4 -2
- package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +4 -3
- package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +4 -3
- package/package.json +4 -4
- package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +4 -3
- package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +4 -3
- package/scripts/dist/nixxie-cms-core-scripts.cjs.js +4 -3
- package/scripts/dist/nixxie-cms-core-scripts.esm.js +4 -3
- package/session/dist/nixxie-cms-core-session.cjs.js +286 -0
- package/session/dist/nixxie-cms-core-session.esm.js +279 -1
- package/src/access.ts +25 -25
- package/src/admin-ui/admin-meta-graphql.ts +5 -5
- package/src/admin-ui/components/CreateButtonLink.tsx +46 -46
- package/src/admin-ui/components/Navigation.tsx +3 -3
- package/src/admin-ui/context.tsx +6 -6
- package/src/admin-ui/utils/Fields.tsx +241 -241
- package/src/admin-ui/utils/actionData.ts +36 -36
- package/src/admin-ui/utils/filters.ts +148 -148
- package/src/admin-ui/utils/useCreateItem.ts +171 -171
- package/src/admin-ui/utils/utils.tsx +127 -127
- package/src/context.ts +1 -1
- package/src/fields/non-null-graphql.ts +115 -115
- package/src/fields/types/bigInt/index.ts +6 -6
- package/src/fields/types/bytes/index.ts +6 -6
- package/src/fields/types/calendarDay/index.ts +18 -19
- package/src/fields/types/checkbox/index.ts +6 -6
- package/src/fields/types/decimal/index.ts +6 -6
- package/src/fields/types/file/index.ts +8 -8
- package/src/fields/types/float/index.ts +6 -6
- package/src/fields/types/image/index.ts +8 -8
- package/src/fields/types/integer/index.ts +6 -6
- package/src/fields/types/json/index.ts +5 -5
- package/src/fields/types/multiselect/index.ts +7 -7
- package/src/fields/types/multiselect/views/index.tsx +149 -151
- package/src/fields/types/password/index.ts +6 -6
- package/src/fields/types/relationship/index.ts +13 -13
- package/src/fields/types/relationship/views/ComboboxMany.tsx +110 -110
- package/src/fields/types/relationship/views/ComboboxSingle.tsx +115 -115
- package/src/fields/types/relationship/views/ContextualActions.tsx +139 -139
- package/src/fields/types/relationship/views/index.tsx +492 -492
- package/src/fields/types/relationship/views/types.ts +46 -46
- package/src/fields/types/relationship/views/useApolloQuery.ts +185 -185
- package/src/fields/types/relationship/views/useFilter.tsx +109 -109
- package/src/fields/types/select/index.ts +6 -6
- package/src/fields/types/text/index.ts +6 -6
- package/src/fields/types/timestamp/index.ts +23 -21
- package/src/fields/types/virtual/index.ts +11 -11
- package/src/helpers.ts +773 -42
- package/src/index.ts +66 -24
- package/src/internal-unstable/admin-ui/pages/ItemPage/common.tsx +4 -4
- package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +5 -5
- package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +8 -8
- package/src/lib/admin-meta.ts +369 -369
- package/src/lib/context/createContext.ts +5 -0
- package/src/lib/core/access-control.ts +434 -434
- package/src/lib/core/cascade.ts +236 -0
- package/src/lib/core/initialise-lists.ts +49 -33
- package/src/lib/core/mutations/index.ts +7 -0
- package/src/lib/core/mutations/nested-mutation-many-input-resolvers.ts +145 -145
- package/src/lib/core/mutations/nested-mutation-one-input-resolvers.ts +71 -71
- package/src/lib/core/queries/output-field.ts +178 -178
- package/src/lib/env.ts +50 -0
- package/src/lib/id-field.ts +2 -2
- package/src/lib/system.ts +221 -207
- package/src/lib/typescript-schema-printer.ts +227 -227
- package/src/list-features.ts +476 -0
- package/src/schema.ts +91 -22
- package/src/session.ts +225 -0
- package/src/types/admin-meta.ts +218 -218
- package/src/types/config/access-control.ts +186 -186
- package/src/types/config/fields.ts +96 -96
- package/src/types/config/hooks.ts +529 -529
- package/src/types/config/index.ts +185 -7
- package/src/types/config/lists.ts +606 -565
- package/src/types/context.ts +426 -55
- package/src/types/next-fields.ts +31 -31
- package/src/types/type-info.ts +38 -38
- package/src/types/type-tests.ts +21 -21
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import * as cookie from 'cookie';
|
|
3
3
|
import Iron from '@hapi/iron';
|
|
4
|
+
import { a as text, j as json, t as timestamp } from '../../dist/index-f1703b7b.esm.js';
|
|
5
|
+
import 'pluralize';
|
|
6
|
+
import '../../dist/next-fields-9bf04ed8.esm.js';
|
|
7
|
+
import 'decimal.js';
|
|
8
|
+
import '@graphql-ts/schema';
|
|
9
|
+
import '@graphql-ts/extend';
|
|
10
|
+
import 'graphql-upload/GraphQLUpload.js';
|
|
11
|
+
import 'graphql';
|
|
12
|
+
import '../../dist/resolve-hooks-9e676794.esm.js';
|
|
13
|
+
import 'node:async_hooks';
|
|
14
|
+
import '../../dist/non-null-graphql-8c5feaae.esm.js';
|
|
15
|
+
import 'node:path';
|
|
4
16
|
|
|
5
17
|
// TODO: should we also accept httpOnly?
|
|
6
18
|
|
|
@@ -77,6 +89,224 @@ function statelessSessions({
|
|
|
77
89
|
}
|
|
78
90
|
};
|
|
79
91
|
}
|
|
92
|
+
function sessionModel(context, collection) {
|
|
93
|
+
var _context$prisma;
|
|
94
|
+
const delegate = (_context$prisma = context.prisma) === null || _context$prisma === void 0 ? void 0 : _context$prisma[collection[0].toLowerCase() + collection.slice(1)];
|
|
95
|
+
if (!delegate) {
|
|
96
|
+
throw new Error(`persistentSessions: collection "${collection}" was not found in the Prisma client. ` + `Add it to your config (e.g. \`collections: { ${collection}: sessionCollection() }\`) and run a migration.`);
|
|
97
|
+
}
|
|
98
|
+
return delegate;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Database-backed sessions: the cookie carries only an opaque token; the session itself
|
|
103
|
+
* lives in a collection, so sessions survive restarts, can be listed per user, and can be
|
|
104
|
+
* revoked server-side (see `listSessions` / `revokeSession` / `revokeUserSessions`).
|
|
105
|
+
*
|
|
106
|
+
* Tracks `lastSeenAt` (refreshed at most once a minute), `userAgent` and `ip` per session.
|
|
107
|
+
* Pair with a scheduled `pruneSessions()` job to clear expired/revoked rows.
|
|
108
|
+
*/
|
|
109
|
+
function persistentSessions({
|
|
110
|
+
collection = 'UserSession',
|
|
111
|
+
maxAge = 60 * 60 * 8,
|
|
112
|
+
// 8 hours
|
|
113
|
+
...statelessSessionsOptions
|
|
114
|
+
} = {}) {
|
|
115
|
+
const stateless = statelessSessions({
|
|
116
|
+
...statelessSessionsOptions,
|
|
117
|
+
maxAge
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
async get({
|
|
121
|
+
context
|
|
122
|
+
}) {
|
|
123
|
+
const token = await stateless.get({
|
|
124
|
+
context
|
|
125
|
+
});
|
|
126
|
+
if (!token) return;
|
|
127
|
+
const row = await sessionModel(context, collection).findUnique({
|
|
128
|
+
where: {
|
|
129
|
+
token
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (!row || row.revokedAt) return;
|
|
133
|
+
if (row.expiresAt && row.expiresAt.getTime() < Date.now()) return;
|
|
134
|
+
|
|
135
|
+
// Refresh activity tracking at most once a minute; never block the request on it.
|
|
136
|
+
if (!row.lastSeenAt || Date.now() - row.lastSeenAt.getTime() > 60000) {
|
|
137
|
+
void sessionModel(context, collection).update({
|
|
138
|
+
where: {
|
|
139
|
+
token
|
|
140
|
+
},
|
|
141
|
+
data: {
|
|
142
|
+
lastSeenAt: new Date()
|
|
143
|
+
}
|
|
144
|
+
}).catch(() => {});
|
|
145
|
+
}
|
|
146
|
+
return row.data;
|
|
147
|
+
},
|
|
148
|
+
async start({
|
|
149
|
+
context,
|
|
150
|
+
data
|
|
151
|
+
}) {
|
|
152
|
+
var _ref, _ref2, _ref3, _forwarded$split$, _req$socket;
|
|
153
|
+
const token = randomBytes(24).toString('base64url'); // 192-bit
|
|
154
|
+
const req = context.req;
|
|
155
|
+
const forwarded = req === null || req === void 0 ? void 0 : req.headers['x-forwarded-for'];
|
|
156
|
+
await sessionModel(context, collection).create({
|
|
157
|
+
data: {
|
|
158
|
+
token,
|
|
159
|
+
data: data,
|
|
160
|
+
itemId: (data === null || data === void 0 ? void 0 : data.itemId) != null ? String(data.itemId) : null,
|
|
161
|
+
expiresAt: new Date(Date.now() + maxAge * 1000),
|
|
162
|
+
createdAt: new Date(),
|
|
163
|
+
lastSeenAt: new Date(),
|
|
164
|
+
userAgent: (_ref = req === null || req === void 0 ? void 0 : req.headers['user-agent']) !== null && _ref !== void 0 ? _ref : null,
|
|
165
|
+
ip: (_ref2 = (_ref3 = typeof forwarded === 'string' ? (_forwarded$split$ = forwarded.split(',')[0]) === null || _forwarded$split$ === void 0 ? void 0 : _forwarded$split$.trim() : forwarded === null || forwarded === void 0 ? void 0 : forwarded[0]) !== null && _ref3 !== void 0 ? _ref3 : req === null || req === void 0 || (_req$socket = req.socket) === null || _req$socket === void 0 ? void 0 : _req$socket.remoteAddress) !== null && _ref2 !== void 0 ? _ref2 : null
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return (await stateless.start({
|
|
169
|
+
context,
|
|
170
|
+
data: token
|
|
171
|
+
})) || '';
|
|
172
|
+
},
|
|
173
|
+
async end({
|
|
174
|
+
context
|
|
175
|
+
}) {
|
|
176
|
+
const token = await stateless.get({
|
|
177
|
+
context
|
|
178
|
+
});
|
|
179
|
+
if (token) {
|
|
180
|
+
await sessionModel(context, collection).deleteMany({
|
|
181
|
+
where: {
|
|
182
|
+
token
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
await stateless.end({
|
|
187
|
+
context
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const stripToken = row => {
|
|
193
|
+
var _row$itemId, _row$lastSeenAt, _row$expiresAt, _row$revokedAt, _row$userAgent, _row$ip;
|
|
194
|
+
return {
|
|
195
|
+
id: row.id,
|
|
196
|
+
itemId: (_row$itemId = row.itemId) !== null && _row$itemId !== void 0 ? _row$itemId : null,
|
|
197
|
+
createdAt: row.createdAt,
|
|
198
|
+
lastSeenAt: (_row$lastSeenAt = row.lastSeenAt) !== null && _row$lastSeenAt !== void 0 ? _row$lastSeenAt : null,
|
|
199
|
+
expiresAt: (_row$expiresAt = row.expiresAt) !== null && _row$expiresAt !== void 0 ? _row$expiresAt : null,
|
|
200
|
+
revokedAt: (_row$revokedAt = row.revokedAt) !== null && _row$revokedAt !== void 0 ? _row$revokedAt : null,
|
|
201
|
+
userAgent: (_row$userAgent = row.userAgent) !== null && _row$userAgent !== void 0 ? _row$userAgent : null,
|
|
202
|
+
ip: (_row$ip = row.ip) !== null && _row$ip !== void 0 ? _row$ip : null
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** Active (non-revoked, non-expired) sessions for a user, without their tokens. */
|
|
207
|
+
async function listSessions(context, itemId, {
|
|
208
|
+
collection = 'UserSession'
|
|
209
|
+
} = {}) {
|
|
210
|
+
const rows = await sessionModel(context, collection).findMany({
|
|
211
|
+
where: {
|
|
212
|
+
itemId: String(itemId),
|
|
213
|
+
revokedAt: null,
|
|
214
|
+
expiresAt: {
|
|
215
|
+
gte: new Date()
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
orderBy: {
|
|
219
|
+
lastSeenAt: 'desc'
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return rows.map(stripToken);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Revoke a single session by its row id. The next request with its cookie is signed out. */
|
|
226
|
+
async function revokeSession(context, sessionId, {
|
|
227
|
+
collection = 'UserSession'
|
|
228
|
+
} = {}) {
|
|
229
|
+
await sessionModel(context, collection).updateMany({
|
|
230
|
+
where: {
|
|
231
|
+
id: sessionId
|
|
232
|
+
},
|
|
233
|
+
data: {
|
|
234
|
+
revokedAt: new Date()
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Revoke every session belonging to a user (e.g. after a password change). */
|
|
240
|
+
async function revokeUserSessions(context, itemId, {
|
|
241
|
+
collection = 'UserSession'
|
|
242
|
+
} = {}) {
|
|
243
|
+
const result = await sessionModel(context, collection).updateMany({
|
|
244
|
+
where: {
|
|
245
|
+
itemId: String(itemId),
|
|
246
|
+
revokedAt: null
|
|
247
|
+
},
|
|
248
|
+
data: {
|
|
249
|
+
revokedAt: new Date()
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
return result.count;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Delete expired and revoked session rows. Run from a scheduled job. */
|
|
256
|
+
async function pruneSessions(context, {
|
|
257
|
+
collection = 'UserSession'
|
|
258
|
+
} = {}) {
|
|
259
|
+
const result = await sessionModel(context, collection).deleteMany({
|
|
260
|
+
where: {
|
|
261
|
+
OR: [{
|
|
262
|
+
expiresAt: {
|
|
263
|
+
lt: new Date()
|
|
264
|
+
}
|
|
265
|
+
}, {
|
|
266
|
+
revokedAt: {
|
|
267
|
+
not: null
|
|
268
|
+
}
|
|
269
|
+
}]
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return result.count;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Sign the current user in as another user, recording who is impersonating in the
|
|
277
|
+
* session (`impersonatedBy`). Works with any session strategy. The caller is
|
|
278
|
+
* responsible for authorising the action (e.g. an admin-only check).
|
|
279
|
+
*/
|
|
280
|
+
async function startImpersonation(context, itemId) {
|
|
281
|
+
if (!context.sessionStrategy) throw new Error('startImpersonation: no session strategy configured');
|
|
282
|
+
const current = context.session;
|
|
283
|
+
if (!(current !== null && current !== void 0 && current.itemId)) throw new Error('startImpersonation: there is no signed-in session');
|
|
284
|
+
if (current.impersonatedBy != null) {
|
|
285
|
+
throw new Error('startImpersonation: already impersonating — end the current impersonation first');
|
|
286
|
+
}
|
|
287
|
+
await context.sessionStrategy.start({
|
|
288
|
+
context,
|
|
289
|
+
data: {
|
|
290
|
+
itemId,
|
|
291
|
+
impersonatedBy: current.itemId
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Return from an impersonated session to the original user's session. */
|
|
297
|
+
async function endImpersonation(context) {
|
|
298
|
+
if (!context.sessionStrategy) throw new Error('endImpersonation: no session strategy configured');
|
|
299
|
+
const current = context.session;
|
|
300
|
+
if ((current === null || current === void 0 ? void 0 : current.impersonatedBy) == null) {
|
|
301
|
+
throw new Error('endImpersonation: the current session is not impersonating anyone');
|
|
302
|
+
}
|
|
303
|
+
await context.sessionStrategy.start({
|
|
304
|
+
context,
|
|
305
|
+
data: {
|
|
306
|
+
itemId: current.impersonatedBy
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
80
310
|
function storedSessions({
|
|
81
311
|
store: storeFn,
|
|
82
312
|
maxAge = 60 * 60 * 8,
|
|
@@ -126,4 +356,52 @@ function storedSessions({
|
|
|
126
356
|
};
|
|
127
357
|
}
|
|
128
358
|
|
|
129
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Ready-made collection definition backing `persistentSessions()`.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* config({
|
|
364
|
+
* collections: {
|
|
365
|
+
* UserSession: sessionCollection(),
|
|
366
|
+
* ...collections,
|
|
367
|
+
* },
|
|
368
|
+
* session: persistentSessions({ collection: 'UserSession' }),
|
|
369
|
+
* })
|
|
370
|
+
*/
|
|
371
|
+
function sessionCollection() {
|
|
372
|
+
return {
|
|
373
|
+
fields: {
|
|
374
|
+
token: text({
|
|
375
|
+
validation: {
|
|
376
|
+
isRequired: true
|
|
377
|
+
},
|
|
378
|
+
isIndexed: 'unique'
|
|
379
|
+
}),
|
|
380
|
+
data: json(),
|
|
381
|
+
itemId: text({
|
|
382
|
+
isIndexed: true
|
|
383
|
+
}),
|
|
384
|
+
expiresAt: timestamp(),
|
|
385
|
+
createdAt: timestamp({
|
|
386
|
+
defaultValue: {
|
|
387
|
+
kind: 'now'
|
|
388
|
+
},
|
|
389
|
+
db: {
|
|
390
|
+
isNullable: false
|
|
391
|
+
}
|
|
392
|
+
}),
|
|
393
|
+
lastSeenAt: timestamp(),
|
|
394
|
+
revokedAt: timestamp(),
|
|
395
|
+
userAgent: text(),
|
|
396
|
+
ip: text()
|
|
397
|
+
},
|
|
398
|
+
graphql: {
|
|
399
|
+
omit: true
|
|
400
|
+
},
|
|
401
|
+
ui: {
|
|
402
|
+
hideNavigation: true
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export { endImpersonation, listSessions, persistentSessions, pruneSessions, revokeSession, revokeUserSessions, sessionCollection, startImpersonation, statelessSessions, storedSessions };
|
package/src/access.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import type { MaybePromise } from './types/utils'
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
export function allowAll() {
|
|
5
|
-
return true
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function denyAll() {
|
|
9
|
-
return false
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function unfiltered<
|
|
13
|
-
boolean |
|
|
14
|
-
> {
|
|
15
|
-
return true
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function allOperations<F>(f: F) {
|
|
19
|
-
return {
|
|
20
|
-
query: f,
|
|
21
|
-
create: f,
|
|
22
|
-
update: f,
|
|
23
|
-
delete: f,
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
import type { MaybePromise } from './types/utils'
|
|
2
|
+
import type { BaseCollectionTypeInfo } from './types'
|
|
3
|
+
|
|
4
|
+
export function allowAll() {
|
|
5
|
+
return true
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function denyAll() {
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function unfiltered<CollectionTypeInfo extends BaseCollectionTypeInfo>(): MaybePromise<
|
|
13
|
+
boolean | CollectionTypeInfo['inputs']['where']
|
|
14
|
+
> {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function allOperations<F>(f: F) {
|
|
19
|
+
return {
|
|
20
|
+
query: f,
|
|
21
|
+
create: f,
|
|
22
|
+
update: f,
|
|
23
|
+
delete: f,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GraphQLNames, JSONValue } from '../types/utils'
|
|
2
|
-
import type {
|
|
2
|
+
import type { CollectionMeta, FieldMeta, FieldGroupMeta } from '../types'
|
|
3
3
|
import { gql } from './apollo'
|
|
4
4
|
|
|
5
5
|
export const adminMetaQuery = gql`
|
|
@@ -142,9 +142,9 @@ export const adminMetaQuery = gql`
|
|
|
142
142
|
export type AdminMetaQuery = {
|
|
143
143
|
nixxie: {
|
|
144
144
|
adminMeta: {
|
|
145
|
-
lists: (
|
|
146
|
-
fields:
|
|
147
|
-
actions:
|
|
145
|
+
lists: (CollectionMeta & {
|
|
146
|
+
fields: CollectionMeta['fields'][string][]
|
|
147
|
+
actions: CollectionMeta['actions']
|
|
148
148
|
groups: (FieldGroupMeta & {
|
|
149
149
|
fields: FieldMeta[]
|
|
150
150
|
})[]
|
|
@@ -155,7 +155,7 @@ export type AdminMetaQuery = {
|
|
|
155
155
|
pageSize: number
|
|
156
156
|
initialColumns: string[]
|
|
157
157
|
initialSearchFields: string[]
|
|
158
|
-
initialSort:
|
|
158
|
+
initialSort: CollectionMeta['initialSort'] | null
|
|
159
159
|
initialFilter: JSONValue
|
|
160
160
|
isSingleton: boolean
|
|
161
161
|
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { css } from '@keystar/ui/style'
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
export function CreateButtonLink(props: { children?: string; list:
|
|
6
|
-
const { list, children = `New ${list.singular}` } = props
|
|
7
|
-
return (
|
|
8
|
-
<a
|
|
9
|
-
href={`/${list.path}/create`}
|
|
10
|
-
aria-label={`New ${list.singular}`}
|
|
11
|
-
className={css({
|
|
12
|
-
display: 'inline-flex',
|
|
13
|
-
alignItems: 'center',
|
|
14
|
-
gap: 6,
|
|
15
|
-
paddingInline: '13px',
|
|
16
|
-
paddingBlock: '7px',
|
|
17
|
-
borderRadius: 6,
|
|
18
|
-
border: '1px solid transparent',
|
|
19
|
-
backgroundColor: '#111827',
|
|
20
|
-
color: '#ffffff',
|
|
21
|
-
fontSize: 13,
|
|
22
|
-
fontWeight: 500,
|
|
23
|
-
fontFamily: 'inherit',
|
|
24
|
-
textDecoration: 'none',
|
|
25
|
-
cursor: 'pointer',
|
|
26
|
-
whiteSpace: 'nowrap',
|
|
27
|
-
flexShrink: 0,
|
|
28
|
-
letterSpacing: '-0.01em',
|
|
29
|
-
transition: 'background 130ms',
|
|
30
|
-
|
|
31
|
-
'&:hover': { backgroundColor: '#1f2937' },
|
|
32
|
-
'&:active': { backgroundColor: '#374151' },
|
|
33
|
-
|
|
34
|
-
'&:focus-visible': {
|
|
35
|
-
outline: '2px solid #111827',
|
|
36
|
-
outlineOffset: 2,
|
|
37
|
-
},
|
|
38
|
-
})}
|
|
39
|
-
>
|
|
40
|
-
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
|
|
41
|
-
<path d="M6 1.5v9M1.5 6h9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
42
|
-
</svg>
|
|
43
|
-
{children}
|
|
44
|
-
</a>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
1
|
+
import { css } from '@keystar/ui/style'
|
|
2
|
+
|
|
3
|
+
import type { CollectionMeta } from '../../types'
|
|
4
|
+
|
|
5
|
+
export function CreateButtonLink(props: { children?: string; list: CollectionMeta }) {
|
|
6
|
+
const { list, children = `New ${list.singular}` } = props
|
|
7
|
+
return (
|
|
8
|
+
<a
|
|
9
|
+
href={`/${list.path}/create`}
|
|
10
|
+
aria-label={`New ${list.singular}`}
|
|
11
|
+
className={css({
|
|
12
|
+
display: 'inline-flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
gap: 6,
|
|
15
|
+
paddingInline: '13px',
|
|
16
|
+
paddingBlock: '7px',
|
|
17
|
+
borderRadius: 6,
|
|
18
|
+
border: '1px solid transparent',
|
|
19
|
+
backgroundColor: '#111827',
|
|
20
|
+
color: '#ffffff',
|
|
21
|
+
fontSize: 13,
|
|
22
|
+
fontWeight: 500,
|
|
23
|
+
fontFamily: 'inherit',
|
|
24
|
+
textDecoration: 'none',
|
|
25
|
+
cursor: 'pointer',
|
|
26
|
+
whiteSpace: 'nowrap',
|
|
27
|
+
flexShrink: 0,
|
|
28
|
+
letterSpacing: '-0.01em',
|
|
29
|
+
transition: 'background 130ms',
|
|
30
|
+
|
|
31
|
+
'&:hover': { backgroundColor: '#1f2937' },
|
|
32
|
+
'&:active': { backgroundColor: '#374151' },
|
|
33
|
+
|
|
34
|
+
'&:focus-visible': {
|
|
35
|
+
outline: '2px solid #111827',
|
|
36
|
+
outlineOffset: 2,
|
|
37
|
+
},
|
|
38
|
+
})}
|
|
39
|
+
>
|
|
40
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
|
|
41
|
+
<path d="M6 1.5v9M1.5 6h9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
42
|
+
</svg>
|
|
43
|
+
{children}
|
|
44
|
+
</a>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -3,14 +3,14 @@ import { useRouter } from 'next/router'
|
|
|
3
3
|
|
|
4
4
|
import { css } from '@keystar/ui/style'
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { CollectionMeta } from '../../types'
|
|
7
7
|
import { useNixxie } from '../context'
|
|
8
8
|
|
|
9
9
|
// ================================================================
|
|
10
10
|
// Helpers
|
|
11
11
|
// ================================================================
|
|
12
12
|
|
|
13
|
-
export function getHrefFromList(list: Pick<
|
|
13
|
+
export function getHrefFromList(list: Pick<CollectionMeta, 'path' | 'isSingleton'>) {
|
|
14
14
|
return `/${list.path}${list.isSingleton ? '/1' : ''}`
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -237,7 +237,7 @@ function CollectionsSection({
|
|
|
237
237
|
lists,
|
|
238
238
|
onNavItemClick,
|
|
239
239
|
}: {
|
|
240
|
-
lists:
|
|
240
|
+
lists: CollectionMeta[]
|
|
241
241
|
onNavItemClick?: () => void
|
|
242
242
|
}) {
|
|
243
243
|
const [search, setSearch] = useState('')
|
package/src/admin-ui/context.tsx
CHANGED
|
@@ -10,11 +10,11 @@ import { useRouter } from '@nixxie-cms/core/admin-ui/router'
|
|
|
10
10
|
import { snapValueToClosest } from '../internal-unstable/admin-ui/pages/ListPage/PaginationControls'
|
|
11
11
|
import type {
|
|
12
12
|
AdminConfig,
|
|
13
|
-
|
|
13
|
+
BaseCollectionTypeInfo,
|
|
14
14
|
ConditionalFilter,
|
|
15
15
|
ConditionalFilterCase,
|
|
16
16
|
FieldViews,
|
|
17
|
-
|
|
17
|
+
CollectionMeta,
|
|
18
18
|
} from '../types'
|
|
19
19
|
import { type AdminMetaQuery, adminMetaQuery } from './admin-meta-graphql'
|
|
20
20
|
import {
|
|
@@ -32,7 +32,7 @@ type NixxieContextType = {
|
|
|
32
32
|
apiPath: string | null
|
|
33
33
|
error?: ErrorLike | null
|
|
34
34
|
fieldViews: FieldViews
|
|
35
|
-
lists: { [list: string]:
|
|
35
|
+
lists: { [list: string]: CollectionMeta }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const NixxieContext = createContext<NixxieContextType>({
|
|
@@ -246,10 +246,10 @@ export function useListItem(
|
|
|
246
246
|
fieldMode: ConditionalFilter<
|
|
247
247
|
'edit' | 'read' | 'hidden',
|
|
248
248
|
'read' | 'hidden',
|
|
249
|
-
|
|
249
|
+
BaseCollectionTypeInfo
|
|
250
250
|
>
|
|
251
251
|
fieldPosition: 'form' | 'sidebar'
|
|
252
|
-
isRequired: ConditionalFilterCase<
|
|
252
|
+
isRequired: ConditionalFilterCase<BaseCollectionTypeInfo>
|
|
253
253
|
} | null
|
|
254
254
|
}[]
|
|
255
255
|
actions: {
|
|
@@ -258,7 +258,7 @@ export function useListItem(
|
|
|
258
258
|
actionMode: ConditionalFilter<
|
|
259
259
|
'enabled' | 'disabled' | 'hidden',
|
|
260
260
|
'disabled' | 'hidden',
|
|
261
|
-
|
|
261
|
+
BaseCollectionTypeInfo
|
|
262
262
|
>
|
|
263
263
|
} | null
|
|
264
264
|
}[]
|