create-coreback 1.0.1 → 1.0.3
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/.github/workflows/publish.yml +36 -3
- package/README.md +2 -2
- package/dist/generators/envExample.d.ts.map +1 -1
- package/dist/generators/envExample.js +9 -0
- package/dist/generators/envExample.js.map +1 -1
- package/dist/generators/packageJson.d.ts.map +1 -1
- package/dist/generators/packageJson.js +29 -27
- package/dist/generators/packageJson.js.map +1 -1
- package/dist/generators/prisma.d.ts.map +1 -1
- package/dist/generators/prisma.js +6 -1
- package/dist/generators/prisma.js.map +1 -1
- package/dist/generators/sourceFiles.js +481 -9
- package/dist/generators/sourceFiles.js.map +1 -1
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +30 -17
- package/dist/prompts.js.map +1 -1
- package/package.json +1 -1
- package/pnpm-workspace.yaml +2 -0
- package/src/generators/envExample.ts +9 -0
- package/src/generators/packageJson.ts +29 -27
- package/src/generators/prisma.ts +6 -1
- package/src/generators/sourceFiles.ts +486 -9
- package/src/index.ts +12 -12
- package/src/prompts.ts +20 -5
|
@@ -91,7 +91,14 @@ const envSchema = z.object({
|
|
|
91
91
|
PORT: z.coerce.number().default(3000),
|
|
92
92
|
DATABASE_URL: z.string(),
|
|
93
93
|
${config.includeAuth ? ` JWT_SECRET: z.string(),
|
|
94
|
-
JWT_EXPIRES_IN: z.string().default('7d')
|
|
94
|
+
JWT_EXPIRES_IN: z.string().default('7d'),
|
|
95
|
+
EMAIL_HOST: z.string().optional(),
|
|
96
|
+
EMAIL_PORT: z.coerce.number().optional(),
|
|
97
|
+
EMAIL_SECURE: z.coerce.boolean().default(false),
|
|
98
|
+
EMAIL_USER: z.string().optional(),
|
|
99
|
+
EMAIL_PASSWORD: z.string().optional(),
|
|
100
|
+
EMAIL_FROM: z.string().default('noreply@coreback.app'),
|
|
101
|
+
APP_URL: z.string().default('http://localhost:3000'),` : ''}
|
|
95
102
|
RATE_LIMIT_WINDOW_MS: z.coerce.number().default(900000),
|
|
96
103
|
RATE_LIMIT_MAX: z.coerce.number().default(100),
|
|
97
104
|
});
|
|
@@ -425,7 +432,7 @@ export { router as healthRoutes };
|
|
|
425
432
|
const authRoutesContent = `import { Router } from 'express';
|
|
426
433
|
import { authController } from '../controllers/auth.controller.js';
|
|
427
434
|
import { validate } from '../middlewares/validator.js';
|
|
428
|
-
import { registerSchema, loginSchema } from '../validators/auth.validator.js';
|
|
435
|
+
import { registerSchema, loginSchema, emailSchema, resetPasswordSchema } from '../validators/auth.validator.js';
|
|
429
436
|
import { authenticate } from '../middlewares/auth.js';
|
|
430
437
|
|
|
431
438
|
/**
|
|
@@ -514,6 +521,108 @@ router.post('/login', validate(loginSchema), authController.login);
|
|
|
514
521
|
*/
|
|
515
522
|
router.get('/me', authenticate, authController.me);
|
|
516
523
|
|
|
524
|
+
/**
|
|
525
|
+
* @swagger
|
|
526
|
+
* /api/auth/verify-email:
|
|
527
|
+
* get:
|
|
528
|
+
* summary: Verify email address
|
|
529
|
+
* tags: [Auth]
|
|
530
|
+
* parameters:
|
|
531
|
+
* - in: query
|
|
532
|
+
* name: token
|
|
533
|
+
* required: true
|
|
534
|
+
* schema:
|
|
535
|
+
* type: string
|
|
536
|
+
* responses:
|
|
537
|
+
* 200:
|
|
538
|
+
* description: Email verified successfully
|
|
539
|
+
* 400:
|
|
540
|
+
* description: Invalid or expired token
|
|
541
|
+
*/
|
|
542
|
+
router.get('/verify-email', authController.verifyEmail);
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* @swagger
|
|
546
|
+
* /api/auth/resend-verification:
|
|
547
|
+
* post:
|
|
548
|
+
* summary: Resend verification email
|
|
549
|
+
* tags: [Auth]
|
|
550
|
+
* requestBody:
|
|
551
|
+
* required: true
|
|
552
|
+
* content:
|
|
553
|
+
* application/json:
|
|
554
|
+
* schema:
|
|
555
|
+
* type: object
|
|
556
|
+
* required:
|
|
557
|
+
* - email
|
|
558
|
+
* properties:
|
|
559
|
+
* email:
|
|
560
|
+
* type: string
|
|
561
|
+
* format: email
|
|
562
|
+
* responses:
|
|
563
|
+
* 200:
|
|
564
|
+
* description: Verification email sent
|
|
565
|
+
* 400:
|
|
566
|
+
* description: Email already verified
|
|
567
|
+
*/
|
|
568
|
+
router.post('/resend-verification', validate(emailSchema), authController.resendVerification);
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @swagger
|
|
572
|
+
* /api/auth/forgot-password:
|
|
573
|
+
* post:
|
|
574
|
+
* summary: Request password reset
|
|
575
|
+
* tags: [Auth]
|
|
576
|
+
* requestBody:
|
|
577
|
+
* required: true
|
|
578
|
+
* content:
|
|
579
|
+
* application/json:
|
|
580
|
+
* schema:
|
|
581
|
+
* type: object
|
|
582
|
+
* required:
|
|
583
|
+
* - email
|
|
584
|
+
* properties:
|
|
585
|
+
* email:
|
|
586
|
+
* type: string
|
|
587
|
+
* format: email
|
|
588
|
+
* responses:
|
|
589
|
+
* 200:
|
|
590
|
+
* description: Password reset email sent
|
|
591
|
+
*/
|
|
592
|
+
router.post('/forgot-password', validate(emailSchema), authController.forgotPassword);
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* @swagger
|
|
596
|
+
* /api/auth/reset-password:
|
|
597
|
+
* post:
|
|
598
|
+
* summary: Reset password
|
|
599
|
+
* tags: [Auth]
|
|
600
|
+
* parameters:
|
|
601
|
+
* - in: query
|
|
602
|
+
* name: token
|
|
603
|
+
* required: true
|
|
604
|
+
* schema:
|
|
605
|
+
* type: string
|
|
606
|
+
* requestBody:
|
|
607
|
+
* required: true
|
|
608
|
+
* content:
|
|
609
|
+
* application/json:
|
|
610
|
+
* schema:
|
|
611
|
+
* type: object
|
|
612
|
+
* required:
|
|
613
|
+
* - password
|
|
614
|
+
* properties:
|
|
615
|
+
* password:
|
|
616
|
+
* type: string
|
|
617
|
+
* minLength: 8
|
|
618
|
+
* responses:
|
|
619
|
+
* 200:
|
|
620
|
+
* description: Password reset successfully
|
|
621
|
+
* 400:
|
|
622
|
+
* description: Invalid or expired token
|
|
623
|
+
*/
|
|
624
|
+
router.post('/reset-password', validate(resetPasswordSchema), authController.resetPassword);
|
|
625
|
+
|
|
517
626
|
export { router as authRoutes };
|
|
518
627
|
`;
|
|
519
628
|
|
|
@@ -551,6 +660,18 @@ export const loginSchema = z.object({
|
|
|
551
660
|
password: z.string().min(1, 'Password is required'),
|
|
552
661
|
}),
|
|
553
662
|
});
|
|
663
|
+
|
|
664
|
+
export const emailSchema = z.object({
|
|
665
|
+
body: z.object({
|
|
666
|
+
email: z.string().email('Invalid email format'),
|
|
667
|
+
}),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
export const resetPasswordSchema = z.object({
|
|
671
|
+
body: z.object({
|
|
672
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
673
|
+
}),
|
|
674
|
+
});
|
|
554
675
|
`;
|
|
555
676
|
|
|
556
677
|
await fs.writeFile(path.join(validatorsDir, 'auth.validator.ts'), authValidatorContent);
|
|
@@ -630,6 +751,77 @@ export const authController = {
|
|
|
630
751
|
throw new AppError(500, 'Failed to get user');
|
|
631
752
|
}
|
|
632
753
|
},
|
|
754
|
+
|
|
755
|
+
verifyEmail: async (req: AuthRequest, res: Response) => {
|
|
756
|
+
try {
|
|
757
|
+
const { token } = req.query;
|
|
758
|
+
if (!token || typeof token !== 'string') {
|
|
759
|
+
throw new AppError(400, 'Verification token is required');
|
|
760
|
+
}
|
|
761
|
+
const result = await authService.verifyEmail(token);
|
|
762
|
+
res.json({
|
|
763
|
+
status: 'success',
|
|
764
|
+
data: result,
|
|
765
|
+
});
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (error instanceof AppError) {
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
throw new AppError(500, 'Failed to verify email');
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
resendVerification: async (req: AuthRequest, res: Response) => {
|
|
775
|
+
try {
|
|
776
|
+
const { email } = req.body;
|
|
777
|
+
const result = await authService.resendVerificationEmail(email);
|
|
778
|
+
res.json({
|
|
779
|
+
status: 'success',
|
|
780
|
+
data: result,
|
|
781
|
+
});
|
|
782
|
+
} catch (error) {
|
|
783
|
+
if (error instanceof AppError) {
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
throw new AppError(500, 'Failed to resend verification email');
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
forgotPassword: async (req: AuthRequest, res: Response) => {
|
|
791
|
+
try {
|
|
792
|
+
const { email } = req.body;
|
|
793
|
+
const result = await authService.requestPasswordReset(email);
|
|
794
|
+
res.json({
|
|
795
|
+
status: 'success',
|
|
796
|
+
data: result,
|
|
797
|
+
});
|
|
798
|
+
} catch (error) {
|
|
799
|
+
if (error instanceof AppError) {
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
throw new AppError(500, 'Failed to send password reset email');
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
resetPassword: async (req: AuthRequest, res: Response) => {
|
|
807
|
+
try {
|
|
808
|
+
const { token } = req.query;
|
|
809
|
+
const { password } = req.body;
|
|
810
|
+
if (!token || typeof token !== 'string') {
|
|
811
|
+
throw new AppError(400, 'Reset token is required');
|
|
812
|
+
}
|
|
813
|
+
const result = await authService.resetPassword(token, password);
|
|
814
|
+
res.json({
|
|
815
|
+
status: 'success',
|
|
816
|
+
data: result,
|
|
817
|
+
});
|
|
818
|
+
} catch (error) {
|
|
819
|
+
if (error instanceof AppError) {
|
|
820
|
+
throw error;
|
|
821
|
+
}
|
|
822
|
+
throw new AppError(500, 'Failed to reset password');
|
|
823
|
+
}
|
|
824
|
+
},
|
|
633
825
|
};
|
|
634
826
|
`;
|
|
635
827
|
|
|
@@ -644,10 +836,15 @@ async function generateServices(servicesDir: string, config: ProjectConfig): Pro
|
|
|
644
836
|
if (config.includeAuth) {
|
|
645
837
|
const authServiceContent = `import bcrypt from 'bcrypt';
|
|
646
838
|
import jwt from 'jsonwebtoken';
|
|
839
|
+
import crypto from 'crypto';
|
|
647
840
|
import { config } from '../config/env.js';
|
|
648
841
|
import { userRepository } from '../repositories/user.repository.js';
|
|
842
|
+
import { emailService } from './email.service.js';
|
|
649
843
|
import { AppError } from '../middlewares/errorHandler.js';
|
|
650
844
|
|
|
845
|
+
const generateVerificationToken = () => crypto.randomBytes(32).toString('hex');
|
|
846
|
+
const generateResetToken = () => crypto.randomBytes(32).toString('hex');
|
|
847
|
+
|
|
651
848
|
export const authService = {
|
|
652
849
|
async register(email: string, password: string, name?: string) {
|
|
653
850
|
const existingUser = await userRepository.findByEmail(email);
|
|
@@ -657,25 +854,34 @@ export const authService = {
|
|
|
657
854
|
}
|
|
658
855
|
|
|
659
856
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
857
|
+
const verificationToken = generateVerificationToken();
|
|
858
|
+
const verificationExpires = new Date();
|
|
859
|
+
verificationExpires.setHours(verificationExpires.getHours() + 24); // 24 hours
|
|
860
|
+
|
|
660
861
|
const user = await userRepository.create({
|
|
661
862
|
email,
|
|
662
863
|
password: hashedPassword,
|
|
663
864
|
name,
|
|
865
|
+
emailVerificationToken: verificationToken,
|
|
866
|
+
emailVerificationExpires: verificationExpires,
|
|
664
867
|
});
|
|
665
868
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
869
|
+
// Send verification email
|
|
870
|
+
try {
|
|
871
|
+
await emailService.sendVerificationEmail(email, verificationToken, name);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
// Log error but don't fail registration
|
|
874
|
+
console.error('Failed to send verification email:', error);
|
|
875
|
+
}
|
|
671
876
|
|
|
672
877
|
return {
|
|
673
878
|
user: {
|
|
674
879
|
id: user.id,
|
|
675
880
|
email: user.email,
|
|
676
881
|
name: user.name,
|
|
882
|
+
emailVerified: user.emailVerified,
|
|
677
883
|
},
|
|
678
|
-
|
|
884
|
+
message: 'Registration successful. Please check your email to verify your account.',
|
|
679
885
|
};
|
|
680
886
|
},
|
|
681
887
|
|
|
@@ -692,6 +898,10 @@ export const authService = {
|
|
|
692
898
|
throw new AppError(401, 'Invalid credentials');
|
|
693
899
|
}
|
|
694
900
|
|
|
901
|
+
if (!user.emailVerified) {
|
|
902
|
+
throw new AppError(403, 'Please verify your email before logging in');
|
|
903
|
+
}
|
|
904
|
+
|
|
695
905
|
const token = jwt.sign(
|
|
696
906
|
{ id: user.id, email: user.email },
|
|
697
907
|
config.JWT_SECRET,
|
|
@@ -703,11 +913,101 @@ export const authService = {
|
|
|
703
913
|
id: user.id,
|
|
704
914
|
email: user.email,
|
|
705
915
|
name: user.name,
|
|
916
|
+
emailVerified: user.emailVerified,
|
|
706
917
|
},
|
|
707
918
|
token,
|
|
708
919
|
};
|
|
709
920
|
},
|
|
710
921
|
|
|
922
|
+
async verifyEmail(token: string) {
|
|
923
|
+
const user = await userRepository.findByVerificationToken(token);
|
|
924
|
+
|
|
925
|
+
if (!user) {
|
|
926
|
+
throw new AppError(400, 'Invalid or expired verification token');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
await userRepository.updateEmailVerification(user.id, true);
|
|
930
|
+
|
|
931
|
+
return {
|
|
932
|
+
message: 'Email verified successfully',
|
|
933
|
+
};
|
|
934
|
+
},
|
|
935
|
+
|
|
936
|
+
async resendVerificationEmail(email: string) {
|
|
937
|
+
const user = await userRepository.findByEmail(email);
|
|
938
|
+
|
|
939
|
+
if (!user) {
|
|
940
|
+
// Don't reveal if user exists
|
|
941
|
+
return {
|
|
942
|
+
message: 'If an account exists with this email, a verification link has been sent.',
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (user.emailVerified) {
|
|
947
|
+
throw new AppError(400, 'Email is already verified');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const verificationToken = generateVerificationToken();
|
|
951
|
+
const verificationExpires = new Date();
|
|
952
|
+
verificationExpires.setHours(verificationExpires.getHours() + 24);
|
|
953
|
+
|
|
954
|
+
await userRepository.updateVerificationToken(user.id, verificationToken, verificationExpires);
|
|
955
|
+
|
|
956
|
+
try {
|
|
957
|
+
await emailService.sendVerificationEmail(email, verificationToken, user.name || undefined);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
console.error('Failed to send verification email:', error);
|
|
960
|
+
throw new AppError(500, 'Failed to send verification email');
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
message: 'Verification email sent',
|
|
965
|
+
};
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
async requestPasswordReset(email: string) {
|
|
969
|
+
const user = await userRepository.findByEmail(email);
|
|
970
|
+
|
|
971
|
+
if (!user) {
|
|
972
|
+
// Don't reveal if user exists
|
|
973
|
+
return {
|
|
974
|
+
message: 'If an account exists with this email, a password reset link has been sent.',
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const resetToken = generateResetToken();
|
|
979
|
+
const resetExpires = new Date();
|
|
980
|
+
resetExpires.setHours(resetExpires.getHours() + 1); // 1 hour
|
|
981
|
+
|
|
982
|
+
await userRepository.updatePasswordResetToken(user.id, resetToken, resetExpires);
|
|
983
|
+
|
|
984
|
+
try {
|
|
985
|
+
await emailService.sendPasswordResetEmail(email, resetToken, user.name || undefined);
|
|
986
|
+
} catch (error) {
|
|
987
|
+
console.error('Failed to send password reset email:', error);
|
|
988
|
+
throw new AppError(500, 'Failed to send password reset email');
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return {
|
|
992
|
+
message: 'Password reset email sent',
|
|
993
|
+
};
|
|
994
|
+
},
|
|
995
|
+
|
|
996
|
+
async resetPassword(token: string, newPassword: string) {
|
|
997
|
+
const user = await userRepository.findByPasswordResetToken(token);
|
|
998
|
+
|
|
999
|
+
if (!user) {
|
|
1000
|
+
throw new AppError(400, 'Invalid or expired reset token');
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
1004
|
+
await userRepository.updatePassword(user.id, hashedPassword);
|
|
1005
|
+
|
|
1006
|
+
return {
|
|
1007
|
+
message: 'Password reset successfully',
|
|
1008
|
+
};
|
|
1009
|
+
},
|
|
1010
|
+
|
|
711
1011
|
async getUserById(id: string) {
|
|
712
1012
|
const user = await userRepository.findById(id);
|
|
713
1013
|
|
|
@@ -730,6 +1030,118 @@ export const authService = {
|
|
|
730
1030
|
path.join(servicesDir, 'auth.service.ts'),
|
|
731
1031
|
authServiceContent
|
|
732
1032
|
);
|
|
1033
|
+
|
|
1034
|
+
// email.service.ts
|
|
1035
|
+
const emailServiceContent = `import nodemailer from 'nodemailer';
|
|
1036
|
+
import { config } from '../config/env.js';
|
|
1037
|
+
|
|
1038
|
+
const createTransporter = () => {
|
|
1039
|
+
// If email is not configured, use a test account (for development)
|
|
1040
|
+
if (!config.EMAIL_HOST) {
|
|
1041
|
+
return nodemailer.createTransporter({
|
|
1042
|
+
host: 'smtp.ethereal.email',
|
|
1043
|
+
port: 587,
|
|
1044
|
+
secure: false,
|
|
1045
|
+
auth: {
|
|
1046
|
+
user: 'test@ethereal.email',
|
|
1047
|
+
pass: 'test',
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return nodemailer.createTransporter({
|
|
1053
|
+
host: config.EMAIL_HOST,
|
|
1054
|
+
port: config.EMAIL_PORT,
|
|
1055
|
+
secure: config.EMAIL_SECURE,
|
|
1056
|
+
auth: config.EMAIL_USER && config.EMAIL_PASSWORD ? {
|
|
1057
|
+
user: config.EMAIL_USER,
|
|
1058
|
+
pass: config.EMAIL_PASSWORD,
|
|
1059
|
+
} : undefined,
|
|
1060
|
+
});
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
const transporter = createTransporter();
|
|
1064
|
+
|
|
1065
|
+
export const emailService = {
|
|
1066
|
+
async sendVerificationEmail(email: string, token: string, name?: string) {
|
|
1067
|
+
const verificationUrl = \`\${config.APP_URL}/api/auth/verify-email?token=\${token}\`;
|
|
1068
|
+
|
|
1069
|
+
const mailOptions = {
|
|
1070
|
+
from: config.EMAIL_FROM,
|
|
1071
|
+
to: email,
|
|
1072
|
+
subject: 'Verify your email address',
|
|
1073
|
+
html: \`
|
|
1074
|
+
<!DOCTYPE html>
|
|
1075
|
+
<html>
|
|
1076
|
+
<head>
|
|
1077
|
+
<meta charset="utf-8">
|
|
1078
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1079
|
+
</head>
|
|
1080
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
1081
|
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
|
1082
|
+
<h1 style="color: white; margin: 0;">Welcome to CoreBack!</h1>
|
|
1083
|
+
</div>
|
|
1084
|
+
<div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px;">
|
|
1085
|
+
<h2 style="color: #333; margin-top: 0;">Hello \${name || 'there'}!</h2>
|
|
1086
|
+
<p>Thank you for registering. Please verify your email address by clicking the button below:</p>
|
|
1087
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
1088
|
+
<a href="\${verificationUrl}" style="background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">Verify Email</a>
|
|
1089
|
+
</div>
|
|
1090
|
+
<p style="color: #666; font-size: 14px;">Or copy and paste this link into your browser:</p>
|
|
1091
|
+
<p style="color: #667eea; font-size: 12px; word-break: break-all;">\${verificationUrl}</p>
|
|
1092
|
+
<p style="color: #666; font-size: 12px; margin-top: 30px;">This link will expire in 24 hours.</p>
|
|
1093
|
+
</div>
|
|
1094
|
+
</body>
|
|
1095
|
+
</html>
|
|
1096
|
+
\`,
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
await transporter.sendMail(mailOptions);
|
|
1100
|
+
},
|
|
1101
|
+
|
|
1102
|
+
async sendPasswordResetEmail(email: string, token: string, name?: string) {
|
|
1103
|
+
const resetUrl = \`\${config.APP_URL}/api/auth/reset-password?token=\${token}\`;
|
|
1104
|
+
|
|
1105
|
+
const mailOptions = {
|
|
1106
|
+
from: config.EMAIL_FROM,
|
|
1107
|
+
to: email,
|
|
1108
|
+
subject: 'Reset your password',
|
|
1109
|
+
html: \`
|
|
1110
|
+
<!DOCTYPE html>
|
|
1111
|
+
<html>
|
|
1112
|
+
<head>
|
|
1113
|
+
<meta charset="utf-8">
|
|
1114
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1115
|
+
</head>
|
|
1116
|
+
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
1117
|
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
|
1118
|
+
<h1 style="color: white; margin: 0;">Password Reset</h1>
|
|
1119
|
+
</div>
|
|
1120
|
+
<div style="background: #f9f9f9; padding: 30px; border-radius: 0 0 10px 10px;">
|
|
1121
|
+
<h2 style="color: #333; margin-top: 0;">Hello \${name || 'there'}!</h2>
|
|
1122
|
+
<p>You requested to reset your password. Click the button below to reset it:</p>
|
|
1123
|
+
<div style="text-align: center; margin: 30px 0;">
|
|
1124
|
+
<a href="\${resetUrl}" style="background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">Reset Password</a>
|
|
1125
|
+
</div>
|
|
1126
|
+
<p style="color: #666; font-size: 14px;">Or copy and paste this link into your browser:</p>
|
|
1127
|
+
<p style="color: #667eea; font-size: 12px; word-break: break-all;">\${resetUrl}</p>
|
|
1128
|
+
<p style="color: #666; font-size: 12px; margin-top: 30px;">This link will expire in 1 hour.</p>
|
|
1129
|
+
<p style="color: #999; font-size: 12px; margin-top: 30px;">If you didn't request this, please ignore this email.</p>
|
|
1130
|
+
</div>
|
|
1131
|
+
</body>
|
|
1132
|
+
</html>
|
|
1133
|
+
\`,
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
await transporter.sendMail(mailOptions);
|
|
1137
|
+
},
|
|
1138
|
+
};
|
|
1139
|
+
`;
|
|
1140
|
+
|
|
1141
|
+
await fs.writeFile(
|
|
1142
|
+
path.join(servicesDir, 'email.service.ts'),
|
|
1143
|
+
emailServiceContent
|
|
1144
|
+
);
|
|
733
1145
|
}
|
|
734
1146
|
}
|
|
735
1147
|
|
|
@@ -757,18 +1169,83 @@ export const userRepository = {
|
|
|
757
1169
|
});
|
|
758
1170
|
},
|
|
759
1171
|
|
|
760
|
-
async create(data: { email: string; password: string; name?: string }) {
|
|
1172
|
+
async create(data: { email: string; password: string; name?: string; emailVerificationToken?: string; emailVerificationExpires?: Date }) {
|
|
761
1173
|
return prisma.user.create({
|
|
762
1174
|
data,
|
|
763
1175
|
select: {
|
|
764
1176
|
id: true,
|
|
765
1177
|
email: true,
|
|
766
1178
|
name: true,
|
|
1179
|
+
emailVerified: true,
|
|
767
1180
|
createdAt: true,
|
|
768
1181
|
updatedAt: true,
|
|
769
1182
|
},
|
|
770
1183
|
});
|
|
771
1184
|
},
|
|
1185
|
+
|
|
1186
|
+
async updateEmailVerification(id: string, verified: boolean) {
|
|
1187
|
+
return prisma.user.update({
|
|
1188
|
+
where: { id },
|
|
1189
|
+
data: {
|
|
1190
|
+
emailVerified: verified,
|
|
1191
|
+
emailVerificationToken: null,
|
|
1192
|
+
emailVerificationExpires: null,
|
|
1193
|
+
},
|
|
1194
|
+
});
|
|
1195
|
+
},
|
|
1196
|
+
|
|
1197
|
+
async findByVerificationToken(token: string) {
|
|
1198
|
+
return prisma.user.findFirst({
|
|
1199
|
+
where: {
|
|
1200
|
+
emailVerificationToken: token,
|
|
1201
|
+
emailVerificationExpires: {
|
|
1202
|
+
gt: new Date(),
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
});
|
|
1206
|
+
},
|
|
1207
|
+
|
|
1208
|
+
async updateVerificationToken(id: string, token: string, expires: Date) {
|
|
1209
|
+
return prisma.user.update({
|
|
1210
|
+
where: { id },
|
|
1211
|
+
data: {
|
|
1212
|
+
emailVerificationToken: token,
|
|
1213
|
+
emailVerificationExpires: expires,
|
|
1214
|
+
},
|
|
1215
|
+
});
|
|
1216
|
+
},
|
|
1217
|
+
|
|
1218
|
+
async updatePasswordResetToken(id: string, token: string, expires: Date) {
|
|
1219
|
+
return prisma.user.update({
|
|
1220
|
+
where: { id },
|
|
1221
|
+
data: {
|
|
1222
|
+
passwordResetToken: token,
|
|
1223
|
+
passwordResetExpires: expires,
|
|
1224
|
+
},
|
|
1225
|
+
});
|
|
1226
|
+
},
|
|
1227
|
+
|
|
1228
|
+
async findByPasswordResetToken(token: string) {
|
|
1229
|
+
return prisma.user.findFirst({
|
|
1230
|
+
where: {
|
|
1231
|
+
passwordResetToken: token,
|
|
1232
|
+
passwordResetExpires: {
|
|
1233
|
+
gt: new Date(),
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
1237
|
+
},
|
|
1238
|
+
|
|
1239
|
+
async updatePassword(id: string, password: string) {
|
|
1240
|
+
return prisma.user.update({
|
|
1241
|
+
where: { id },
|
|
1242
|
+
data: {
|
|
1243
|
+
password,
|
|
1244
|
+
passwordResetToken: null,
|
|
1245
|
+
passwordResetExpires: null,
|
|
1246
|
+
},
|
|
1247
|
+
});
|
|
1248
|
+
},
|
|
772
1249
|
};
|
|
773
1250
|
`;
|
|
774
1251
|
|
package/src/index.ts
CHANGED
|
@@ -7,20 +7,20 @@ import chalk from 'chalk';
|
|
|
7
7
|
const banner = `
|
|
8
8
|
${chalk.cyan.bold('╔═══════════════════════════════════════════════════════════════╗')}
|
|
9
9
|
${chalk.cyan.bold('║')} ${chalk.cyan.bold('║')}
|
|
10
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold(' ██████╗ ██████╗ ██████╗ ███████╗')}
|
|
11
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('██╔════╝██╔═══██╗██╔══██╗██╔════╝')}
|
|
12
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('██║ ██║ ██║██████╔╝█████╗ ')}
|
|
13
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('██║ ██║ ██║██╔══██╗██╔══╝ ')}
|
|
14
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('╚██████╗╚██████╝ ██║ ██║███████╗')}
|
|
10
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold(' ██████╗ ██████╗ ██████╗ ███████╗')} ${chalk.cyan.bold('║')}
|
|
11
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('██╔════╝██╔═══██╗██╔══██╗██╔════╝')} ${chalk.cyan.bold('║')}
|
|
12
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('██║ ██║ ██║██████╔╝█████╗ ')} ${chalk.cyan.bold('║')}
|
|
13
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('██║ ██║ ██║██╔══██╗██╔══╝ ')} ${chalk.cyan.bold('║')}
|
|
14
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('╚██████╗╚██████╝ ██║ ██║███████╗')} ${chalk.cyan.bold('║')}
|
|
15
|
+
${chalk.cyan.bold('║')} ${chalk.cyan.bold('║')}
|
|
16
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('██████╗ █████╗ ██████╗██╗ ██╗')} ${chalk.cyan.bold('║')}
|
|
17
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('██╔══██╗██╔══██╗██╔════╝██║ ██╔╝')} ${chalk.cyan.bold('║')}
|
|
18
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('██████╔╝███████║██║ █████╔╝ ')} ${chalk.cyan.bold('║')}
|
|
19
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('██╔══██╗██╔══██║██║ ██╔═██╗ ')} ${chalk.cyan.bold('║')}
|
|
20
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('██████╔╝██║ ██║╚██████╗██║ ██╗')} ${chalk.cyan.bold('║')}
|
|
21
|
+
${chalk.cyan.bold('║')} ${chalk.white.bold('╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝')} ${chalk.cyan.bold('║')}
|
|
15
22
|
${chalk.cyan.bold('║')} ${chalk.cyan.bold('║')}
|
|
16
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('██████╗ █████╗ ██████╗██╗ ██╗')} ${chalk.cyan.bold('║')}
|
|
17
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('██╔══██╗██╔══██╗██╔════╝██║ ██╔╝')} ${chalk.cyan.bold('║')}
|
|
18
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('██████╔╝███████║██║ █████╔╝ ')} ${chalk.cyan.bold('║')}
|
|
19
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('██╔══██╗██╔══██║██║ ██╔═██╗ ')} ${chalk.cyan.bold('║')}
|
|
20
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('██████╔╝██║ ██║╚██████╗██║ ██╗')} ${chalk.cyan.bold('║')}
|
|
21
|
-
${chalk.cyan.bold('║')} ${chalk.white.bold('╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝')} ${chalk.cyan.bold('║')}
|
|
22
23
|
${chalk.cyan.bold('║')} ${chalk.cyan.bold('║')}
|
|
23
|
-
${chalk.cyan.bold('║')} ${chalk.gray(' Production-Ready Backend Generator')} ${chalk.cyan.bold('║')}
|
|
24
24
|
${chalk.cyan.bold('║')} ${chalk.cyan.bold('║')}
|
|
25
25
|
${chalk.cyan.bold('╚═══════════════════════════════════════════════════════════════╝')}
|
|
26
26
|
`;
|
package/src/prompts.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { ProjectConfig, DatabaseType, PackageManager } from './types.js';
|
|
4
4
|
|
|
5
5
|
export async function promptProjectConfig(
|
|
6
6
|
defaultName?: string
|
|
7
7
|
): Promise<ProjectConfig> {
|
|
8
|
+
// If project name is provided as argument, use it directly without prompting
|
|
9
|
+
const projectName = defaultName
|
|
10
|
+
? defaultName.toLowerCase().trim()
|
|
11
|
+
: undefined;
|
|
12
|
+
|
|
13
|
+
// Validate the provided name if it exists
|
|
14
|
+
if (projectName) {
|
|
15
|
+
if (!/^[a-z0-9-]+$/.test(projectName)) {
|
|
16
|
+
throw new Error('Project name must be lowercase, alphanumeric, and can contain hyphens');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
const answers = await inquirer.prompt([
|
|
9
|
-
{
|
|
21
|
+
...(projectName ? [] : [{
|
|
10
22
|
type: 'input',
|
|
11
23
|
name: 'projectName',
|
|
12
24
|
message: 'Project name:',
|
|
13
|
-
default:
|
|
25
|
+
default: 'my-project',
|
|
14
26
|
validate: (input: string) => {
|
|
15
27
|
if (!input.trim()) {
|
|
16
28
|
return 'Project name cannot be empty';
|
|
@@ -21,7 +33,7 @@ export async function promptProjectConfig(
|
|
|
21
33
|
return true;
|
|
22
34
|
},
|
|
23
35
|
filter: (input: string) => input.toLowerCase().trim(),
|
|
24
|
-
},
|
|
36
|
+
}]),
|
|
25
37
|
{
|
|
26
38
|
type: 'list',
|
|
27
39
|
name: 'database',
|
|
@@ -57,6 +69,9 @@ export async function promptProjectConfig(
|
|
|
57
69
|
},
|
|
58
70
|
]);
|
|
59
71
|
|
|
60
|
-
return
|
|
72
|
+
return {
|
|
73
|
+
...answers,
|
|
74
|
+
projectName: projectName || answers.projectName,
|
|
75
|
+
} as ProjectConfig;
|
|
61
76
|
}
|
|
62
77
|
|