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.
- package/bundle/cli/index.js +224 -362
- package/bundle/res/stdlib.zmodel +1 -1
- package/package.json +5 -3
- package/src/cli/index.ts +1 -1
- package/src/generator/field-constraint/index.ts +13 -6
- package/src/generator/index.ts +15 -8
- package/src/generator/prisma/index.ts +12 -7
- package/src/generator/react-hooks/index.ts +17 -12
- package/src/generator/service/index.ts +10 -3
- package/src/generator/tsc/index.ts +9 -2
- package/src/generator/types.ts +3 -1
- package/src/res/stdlib.zmodel +1 -1
- package/src/generator/next-auth/index.ts +0 -352
|
@@ -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
|
-
}
|