zenstack 0.4.0 → 0.4.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.
@@ -54,7 +54,7 @@ enum AttributeTargetField {
54
54
  function env(name: String): String {}
55
55
 
56
56
  /*
57
- * Gets thec current login user.
57
+ * Gets the current login user.
58
58
  */
59
59
  function auth(): Any {}
60
60
 
package/package.json CHANGED
@@ -2,11 +2,12 @@
2
2
  "name": "zenstack",
3
3
  "publisher": "zenstack",
4
4
  "displayName": "ZenStack Language Tools",
5
- "description": "A toolkit for modeling data and access policies in full-stack development with Next.js and Typescript",
6
- "version": "0.4.0",
5
+ "description": "A toolkit for building secure CRUD apps with Next.js + Typescript",
6
+ "version": "0.4.1",
7
7
  "author": {
8
8
  "name": "ZenStack Team"
9
9
  },
10
+ "homepage": "https://zenstack.dev",
10
11
  "license": "MIT",
11
12
  "keywords": [
12
13
  "fullstack",
@@ -85,7 +86,7 @@
85
86
  "vscode-languageserver": "^8.0.2",
86
87
  "vscode-languageserver-textdocument": "^1.0.7",
87
88
  "vscode-uri": "^3.0.6",
88
- "@zenstackhq/runtime": "0.4.0"
89
+ "@zenstackhq/runtime": "0.4.1"
89
90
  },
90
91
  "devDependencies": {
91
92
  "@prisma/internals": "~4.7.0",
package/src/cli/index.ts CHANGED
@@ -113,7 +113,7 @@ export default async function (): Promise<void> {
113
113
  .description(
114
114
  `${colors.bold.blue(
115
115
  'ζ'
116
- )} ZenStack is a toolkit for building secure CRUD apps with Next.js + Typescript.\n\nDocumentation: https://go.zenstack.dev/doc.`
116
+ )} ZenStack is a toolkit for building secure CRUD apps with Next.js + Typescript.\n\nDocumentation: https://zenstack.dev.`
117
117
  )
118
118
  .showHelpAfterError()
119
119
  .showSuggestionAfterError();
@@ -5,7 +5,6 @@ import colors from 'colors';
5
5
  import PrismaGenerator from './prisma';
6
6
  import ServiceGenerator from './service';
7
7
  import ReactHooksGenerator from './react-hooks';
8
- import NextAuthGenerator from './next-auth';
9
8
  import { TypescriptCompilation } from './tsc';
10
9
  import FieldConstraintGenerator from './field-constraint';
11
10
  import telemetry from '../telemetry';
@@ -46,7 +45,6 @@ export class ZenStackGenerator {
46
45
  new PrismaGenerator(),
47
46
  new ServiceGenerator(),
48
47
  new ReactHooksGenerator(),
49
- new NextAuthGenerator(),
50
48
  new FieldConstraintGenerator(),
51
49
  new TypescriptCompilation(),
52
50
  ];
@@ -54,7 +54,7 @@ enum AttributeTargetField {
54
54
  function env(name: String): String {}
55
55
 
56
56
  /*
57
- * Gets thec current login user.
57
+ * Gets the current login user.
58
58
  */
59
59
  function auth(): Any {}
60
60
 
@@ -1,352 +0,0 @@
1
- import { Context, Generator } from '../types';
2
- import { Project } from 'ts-morph';
3
- import * as path from 'path';
4
- import colors from 'colors';
5
- import { DataModel, isDataModel, Model } from '@lang/generated/ast';
6
- import { execSync } from 'child_process';
7
-
8
- /**
9
- * Generates NextAuth adaptor code
10
- */
11
- export default class NextAuthGenerator implements Generator {
12
- get name() {
13
- return 'next-auth';
14
- }
15
-
16
- private findModel(schema: Model, name: string) {
17
- return schema.declarations.find(
18
- (d) => isDataModel(d) && d.name === name
19
- ) as DataModel;
20
- }
21
-
22
- private modelHasField(model: DataModel, name: string) {
23
- return !!model.fields.find((f) => f.name === name);
24
- }
25
-
26
- async generate(context: Context): Promise<void> {
27
- try {
28
- execSync('npm ls next-auth');
29
- } catch (err) {
30
- return;
31
- }
32
-
33
- if (!this.findModel(context.schema, 'User')) {
34
- console.warn(
35
- colors.yellow(
36
- 'Skipping generating next-auth adapter: "User" model not found.'
37
- )
38
- );
39
- return;
40
- }
41
-
42
- const userModel = this.findModel(context.schema, 'User');
43
- if (
44
- !this.modelHasField(userModel, 'email') ||
45
- !this.modelHasField(userModel, 'emailVerified')
46
- ) {
47
- console.warn(
48
- colors.yellow(
49
- `Skipping generating next-auth adapter because "User" model doesn't meet requirements: "email" and "emailVerified" fields are required.`
50
- )
51
- );
52
- return;
53
- }
54
-
55
- const project = new Project();
56
-
57
- this.generateIndex(project, context);
58
- this.generateAdapter(project, context);
59
- this.generateAuthorize(project, context);
60
-
61
- await project.save();
62
-
63
- console.log(colors.blue(` ✔️ Next-auth adapter generated`));
64
- }
65
-
66
- private generateIndex(project: Project, context: Context) {
67
- const sf = project.createSourceFile(
68
- path.join(context.generatedCodeDir, 'src/auth/index.ts'),
69
- undefined,
70
- { overwrite: true }
71
- );
72
-
73
- sf.addStatements([
74
- `export * from './next-auth-adapter';`,
75
- `export * from './authorize';`,
76
- ]);
77
-
78
- sf.formatText();
79
- }
80
-
81
- private generateAdapter(project: Project, context: Context) {
82
- const sf = project.createSourceFile(
83
- path.join(
84
- context.generatedCodeDir,
85
- 'src/auth/next-auth-adapter.ts'
86
- ),
87
- undefined,
88
- { overwrite: true }
89
- );
90
-
91
- sf.addImportDeclarations([
92
- {
93
- namedImports: [
94
- {
95
- name: 'ZenStackService',
96
- },
97
- ],
98
- moduleSpecifier: '..',
99
- },
100
- {
101
- namedImports: [
102
- {
103
- name: 'Adapter',
104
- },
105
- ],
106
- moduleSpecifier: 'next-auth/adapters',
107
- },
108
- {
109
- namedImports: [
110
- {
111
- name: 'Prisma',
112
- },
113
- ],
114
- moduleSpecifier: '../../.prisma',
115
- isTypeOnly: true,
116
- },
117
- ]);
118
-
119
- const adapter = sf.addFunction({
120
- name: 'NextAuthAdapter',
121
- isExported: true,
122
- returnType: 'Adapter',
123
- });
124
-
125
- adapter.addParameter({
126
- name: 'service',
127
- type: 'ZenStackService',
128
- });
129
-
130
- const userModel = this.findModel(context.schema, 'User');
131
-
132
- adapter.setBodyText((writer) => {
133
- writer.writeLine('const db = service.db;');
134
- writer.write('return ');
135
- writer.block(() => {
136
- writer.writeLine(
137
- `
138
- createUser: (data) => db.user.create({ data: data as Prisma.UserCreateInput }),
139
- getUser: (id) => db.user.findUnique({ where: { id } }),
140
- updateUser: (data) => db.user.update({ where: { id: data.id }, data: data as Prisma.UserUpdateInput }),
141
- deleteUser: (id) => db.user.delete({ where: { id } }),
142
- `
143
- );
144
-
145
- if (this.modelHasField(userModel, 'email')) {
146
- writer.writeLine(
147
- 'getUserByEmail: (id) => db.user.findUnique({ where: { id } }),'
148
- );
149
- } else {
150
- writer.writeLine(
151
- `getUserByEmail: (id) => { throw new Error('"User" model has no "email" field'); },`
152
- );
153
- }
154
-
155
- if (this.findModel(context.schema, 'Account')) {
156
- writer.writeLine(
157
- `
158
- async getUserByAccount(provider_providerAccountId) {
159
- const account = await db.account.findUnique({
160
- where: { provider_providerAccountId },
161
- select: { user: true },
162
- });
163
- return account?.user ?? null;
164
- },
165
- linkAccount: (data) => db.account.create({ data }) as any,
166
- unlinkAccount: (provider_providerAccountId) =>
167
- db.account.delete({ where: { provider_providerAccountId } }) as any,
168
- `
169
- );
170
- } else {
171
- writer.writeLine(
172
- `
173
- async getUserByAccount(provider_providerAccountId) { throw new Error('Schema has no "Account" model declared'); },
174
- linkAccount: (data) => { throw new Error('Schema has no "Account" model declared'); },
175
- unlinkAccount: (provider_providerAccountId) => { throw new Error('Schema has no "Account" model declared'); },
176
- `
177
- );
178
- }
179
-
180
- if (this.findModel(context.schema, 'Session')) {
181
- writer.writeLine(
182
- `
183
- async getSessionAndUser(sessionToken) {
184
- const userAndSession = await db.session.findUnique({
185
- where: { sessionToken },
186
- include: { user: true },
187
- });
188
- if (!userAndSession) return null;
189
- const { user, ...session } = userAndSession;
190
- return { user, session };
191
- },
192
- createSession: (data) => db.session.create({ data }),
193
- updateSession: (data) =>
194
- db.session.update({
195
- data,
196
- where: { sessionToken: data.sessionToken },
197
- }),
198
- deleteSession: (sessionToken) =>
199
- db.session.delete({ where: { sessionToken } }),
200
- `
201
- );
202
- } else {
203
- writer.writeLine(
204
- `
205
- async getSessionAndUser(sessionToken) { throw new Error('Schema has no "Session" model declared'); },
206
- createSession: (data) => { throw new Error('Schema has no "Session" model declared'); },
207
- updateSession: (data) => { throw new Error('Schema has no "Session" model declared'); },
208
- deleteSession: (sessionToken) => { throw new Error('Schema has no "Session" model declared'); }, `
209
- );
210
- }
211
-
212
- if (this.findModel(context.schema, 'VerificationToken')) {
213
- writer.writeLine(
214
- `
215
- createVerificationToken: (data) => db.verificationToken.create({ data }),
216
- async useVerificationToken(identifier_token) {
217
- try {
218
- return await db.verificationToken.delete({
219
- where: { identifier_token },
220
- });
221
- } catch (error) {
222
- // If token already used/deleted, just return null
223
- // https://www.prisma.io/docs/reference/api-reference/error-reference#p2025
224
- if (
225
- (error as Prisma.PrismaClientKnownRequestError).code ===
226
- 'P2025'
227
- )
228
- return null;
229
- throw error;
230
- }
231
- },
232
- `
233
- );
234
- } else {
235
- writer.writeLine(
236
- `
237
- createVerificationToken: (data) => { throw new Error('Schema has no "VerificationToken" model declared'); },
238
- async useVerificationToken(identifier_token) { throw new Error('Schema has no "VerificationToken" model declared'); }, `
239
- );
240
- }
241
- });
242
- });
243
-
244
- sf.formatText();
245
- }
246
-
247
- private generateAuthorize(project: Project, context: Context) {
248
- const userModel = this.findModel(context.schema, 'User');
249
- const hasEmail = userModel && this.modelHasField(userModel, 'email');
250
- const hasPassword =
251
- userModel && this.modelHasField(userModel, 'password');
252
-
253
- let content = '';
254
- if (!hasEmail || !hasPassword) {
255
- content = `
256
- import { ZenStackService } from '..';
257
-
258
- export function authorize(service: ZenStackService, implicitSignup = false) {
259
- throw new Error('"User" model must have "email" and "password" field');
260
- }
261
- `;
262
- } else {
263
- content = `
264
- import { ZenStackService } from '..';
265
- import { hash, compare } from 'bcryptjs';
266
-
267
- async function hashPassword(password: string) {
268
- const hashedPassword = await hash(password, 12);
269
- return hashedPassword;
270
- }
271
-
272
- async function verifyPassword(password: string, hashedPassword: string) {
273
- const isValid = await compare(password, hashedPassword);
274
- return isValid;
275
- }
276
-
277
- export function authorize(service: ZenStackService, implicitSignup = false) {
278
- return async (
279
- credentials: Record<'email' | 'password', string> | undefined
280
- ) => {
281
- if (!credentials) {
282
- throw new Error('Missing credentials');
283
- }
284
-
285
- try {
286
- let maybeUser = await service.db.user.findFirst({
287
- where: {
288
- email: credentials!.email,
289
- },
290
- select: {
291
- id: true,
292
- email: true,
293
- password: true,
294
- name: true,
295
- },
296
- });
297
-
298
- if (!maybeUser) {
299
- if (!implicitSignup || !credentials.password || !credentials.email) {
300
- return null;
301
- }
302
-
303
- maybeUser = await service.db.user.create({
304
- data: {
305
- email: credentials.email,
306
- password: await hashPassword(credentials.password),
307
- },
308
- select: {
309
- id: true,
310
- email: true,
311
- password: true,
312
- name: true,
313
- },
314
- });
315
- } else {
316
- if (!maybeUser.password) {
317
- throw new Error('Invalid User Record');
318
- }
319
-
320
- const isValid = await verifyPassword(
321
- credentials.password,
322
- maybeUser.password
323
- );
324
-
325
- if (!isValid) {
326
- return null;
327
- }
328
- }
329
-
330
- return {
331
- id: maybeUser.id,
332
- email: maybeUser.email,
333
- name: maybeUser.name,
334
- };
335
- } catch (error) {
336
- console.log('Error occurred during authorization:', error);
337
- throw error;
338
- }
339
- };
340
- }
341
- `;
342
- }
343
-
344
- const sf = project.createSourceFile(
345
- path.join(context.generatedCodeDir, 'src/auth/authorize.ts'),
346
- content,
347
- { overwrite: true }
348
- );
349
-
350
- sf.formatText();
351
- }
352
- }