zenstack 0.4.0 → 0.4.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.
@@ -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
- }