Merge branch 'task-84-feature/api_password_recovery' into 'development'

Task 84 feature/api password recovery

See merge request !57
parents d98464d7 33a7f579
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/nodemailer": "^6.4.6",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
...@@ -20,6 +21,7 @@ ...@@ -20,6 +21,7 @@
"mongoose": "^6.7.0", "mongoose": "^6.7.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"nodemailer": "^6.8.0",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^8.8.0", "pg": "^8.8.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
...@@ -1712,6 +1714,14 @@ ...@@ -1712,6 +1714,14 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.8.tgz",
"integrity": "sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A==" "integrity": "sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A=="
}, },
"node_modules/@types/nodemailer": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.6.tgz",
"integrity": "sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.7", "version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
...@@ -4488,6 +4498,14 @@ ...@@ -4488,6 +4498,14 @@
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
} }
}, },
"node_modules/nodemailer": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.8.0.tgz",
"integrity": "sha512-EjYvSmHzekz6VNkNd12aUqAco+bOkRe3Of5jVhltqKhEsjw/y0PYPJfp83+s9Wzh1dspYAkUW/YNQ350NATbSQ==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "2.0.20", "version": "2.0.20",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
...@@ -7663,6 +7681,14 @@ ...@@ -7663,6 +7681,14 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.8.tgz",
"integrity": "sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A==" "integrity": "sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A=="
}, },
"@types/nodemailer": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.6.tgz",
"integrity": "sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==",
"requires": {
"@types/node": "*"
}
},
"@types/qs": { "@types/qs": {
"version": "6.9.7", "version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
...@@ -9721,6 +9747,11 @@ ...@@ -9721,6 +9747,11 @@
} }
} }
}, },
"nodemailer": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.8.0.tgz",
"integrity": "sha512-EjYvSmHzekz6VNkNd12aUqAco+bOkRe3Of5jVhltqKhEsjw/y0PYPJfp83+s9Wzh1dspYAkUW/YNQ350NATbSQ=="
},
"nodemon": { "nodemon": {
"version": "2.0.20", "version": "2.0.20",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/nodemailer": "^6.4.6",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.13.2", "class-validator": "^0.13.2",
...@@ -37,6 +38,7 @@ ...@@ -37,6 +38,7 @@
"mongoose": "^6.7.0", "mongoose": "^6.7.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nanoid": "^3.3.4", "nanoid": "^3.3.4",
"nodemailer": "^6.8.0",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^8.8.0", "pg": "^8.8.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
......
...@@ -4,6 +4,7 @@ import {Task} from './models/Task'; ...@@ -4,6 +4,7 @@ import {Task} from './models/Task';
import { Project } from "./models/Project"; import { Project } from "./models/Project";
import { Member } from "./models/Member"; import { Member } from "./models/Member";
import { DateTimeTask } from "./models/DateTimeTask"; import { DateTimeTask } from "./models/DateTimeTask";
import { PasswordRecovery } from "./models/PasswordRecovery";
export const myDataSource = new DataSource({ export const myDataSource = new DataSource({
type: "postgres", type: "postgres",
...@@ -12,7 +13,7 @@ export const myDataSource = new DataSource({ ...@@ -12,7 +13,7 @@ export const myDataSource = new DataSource({
username: "pluser", username: "pluser",
password: "pluser", password: "pluser",
database: "planner", database: "planner",
entities: [User,Task,Project,Member,DateTimeTask], entities: [User,Task,Project,Member,DateTimeTask,PasswordRecovery],
logging: true, logging: true,
synchronize: true, // in build switch to false synchronize: true, // in build switch to false
migrationsRun: false migrationsRun: false
......
...@@ -2,6 +2,8 @@ import express, { NextFunction, Request, Response, Router } from "express"; ...@@ -2,6 +2,8 @@ import express, { NextFunction, Request, Response, Router } from "express";
import { myDataSource } from "./app-data-source"; import { myDataSource } from "./app-data-source";
import { Task } from "./models/Task"; import { Task } from "./models/Task";
import { User } from "./models/User"; import { User } from "./models/User";
import nodemailer from 'nodemailer';
const dataSource = myDataSource; const dataSource = myDataSource;
...@@ -76,3 +78,20 @@ export const taskFinderById = async (taskId:string):Promise<null | Task>=>{ ...@@ -76,3 +78,20 @@ export const taskFinderById = async (taskId:string):Promise<null | Task>=>{
}) })
return task return task
} }
export let transporter = nodemailer.createTransport({
service:'Yandex',
// host: "smtp.yandex.ru",
// port: 465,
// secure: true, // true for 465, false for other ports
auth: {
user: "planner45@yandex.ru", // generated ethereal user
pass: "newPlannerProject123" // generated ethereal password
}
})
export const FRONTEND_URL = 'localhost:3000'
\ No newline at end of file
import {
Column,
Entity,
PrimaryGeneratedColumn,
BaseEntity,
OneToOne,
CreateDateColumn,
JoinColumn,
} from 'typeorm';
import { User } from './User';
interface IPasswordRecovery{
createdAt:Date;
token: string;
user: User;
enabled:boolean;
}
@Entity({name: 'PasswordRecovery'})
export class PasswordRecovery extends BaseEntity implements IPasswordRecovery{
@PrimaryGeneratedColumn('uuid')
id!: string;
@CreateDateColumn({ name: 'created_at', type: Date, default: new Date() })
createdAt!: Date;
@Column({ name: 'token', type: 'varchar',length:100, unique: true, nullable:true })
token!: string;
@OneToOne(()=>User,{nullable: false, eager:true})
@JoinColumn()
user!:User;
@Column({ name: 'enabled', type:'boolean', default:true})
enabled!: boolean;
}
\ No newline at end of file
...@@ -50,7 +50,7 @@ import { DateTimeTask } from './DateTimeTask'; ...@@ -50,7 +50,7 @@ import { DateTimeTask } from './DateTimeTask';
dateTimeDeadLine!: Date; dateTimeDeadLine!: Date;
@Column({ name: 'dateTimeFactDeadLine', type: Date,nullable: true }) @Column({ name: 'dateTimeFactDeadLine', type: Date,nullable: true })
dateTimeFactDeadLine!: Date; dateTimeFactDeadLine!: Date;
@Column({ name: 'archive', type: 'varchar', length:50,nullable: false, default:false }) @Column({ name: 'archive', type: 'boolean',nullable: false, default:false })
archive!: boolean archive!: boolean
@Column({ @Column({
......
...@@ -5,9 +5,7 @@ import { ...@@ -5,9 +5,7 @@ import {
CreateDateColumn, CreateDateColumn,
BeforeInsert, BeforeInsert,
BaseEntity, BaseEntity,
ManyToMany,
OneToMany, OneToMany,
JoinTable,
OneToOne, OneToOne,
} from 'typeorm'; } from 'typeorm';
import {IsEmail import {IsEmail
...@@ -17,8 +15,9 @@ import bcrypt from 'bcrypt'; ...@@ -17,8 +15,9 @@ import bcrypt from 'bcrypt';
import {nanoid} from 'nanoid'; import {nanoid} from 'nanoid';
import {Task} from './Task'; import {Task} from './Task';
import {Member} from './Member'; import {Member} from './Member';
import { PasswordRecovery } from './PasswordRecovery';
const SALT_WORK_FACTOR= 10; export const SALT_WORK_FACTOR= 10;
export enum UserRole {USER="user" ,DIRECTOR= "director",SUPERUSER="superuser"} export enum UserRole {USER="user" ,DIRECTOR= "director",SUPERUSER="superuser"}
...@@ -38,7 +37,6 @@ interface IUser { ...@@ -38,7 +37,6 @@ interface IUser {
} }
@Entity({ name: 'User' }) @Entity({ name: 'User' })
export class User extends BaseEntity implements IUser { export class User extends BaseEntity implements IUser {
@PrimaryGeneratedColumn("uuid") @PrimaryGeneratedColumn("uuid")
...@@ -76,20 +74,15 @@ export class User extends BaseEntity implements IUser { ...@@ -76,20 +74,15 @@ export class User extends BaseEntity implements IUser {
@Exclude({ toPlainOnly: true }) @Exclude({ toPlainOnly: true })
password!: string; password!: string;
@OneToMany(() => Task, (task: { user: User }) => task.user) @OneToMany(() => Task, (task: { user: User }) => task.user)
createdTasks!: Task[]; createdTasks!: Task[];
@OneToMany(() => Task, (task: { user: User }) =>task.user) @OneToMany(() => Task, (task: { user: User }) =>task.user)
tasks!: Task[]; tasks!: Task[];
@OneToMany(() => Member, (member: { user: User }) => member.user) @OneToMany(() => Member, (member: { user: User }) => member.user)
members!: Member[]; members!: Member[];
// @ManyToMany(() => Project,(project: { user: User }) => project.user)
// @JoinTable()
// workerInProjects!: Project[];
@BeforeInsert() @BeforeInsert()
......
import express,{Router, Request, Response} from 'express';
import {User} from '../models/User';
import {myDataSource} from '../app-data-source';
import { nanoid } from 'nanoid';
import { PasswordRecovery } from '../models/PasswordRecovery';
import { transporter } from '../helpers';
import {FRONTEND_URL} from "../helpers";
import {SALT_WORK_FACTOR} from "../models/User";
import bcrypt from 'bcrypt';
const router:Router = express.Router();
const dataSource = myDataSource;
/**Make requiest to init recovery process */
router.post ('/', async (req:Request, res:Response):Promise<void |Response>=>{
const {email} = req.body
const user = await dataSource
.getRepository(User)
.findOne({
where:{
email:email
}
})
if (!user) return res.status(404).send({message:'user not found'})
const token = nanoid();
try{
const passwordRecovery = new PasswordRecovery()
passwordRecovery.user= user;
passwordRecovery.token=token;
await passwordRecovery.save()
const url = `${FRONTEND_URL}/reset-password/${token}`;
await transporter.sendMail({
from:"planner45@yandex.com",
to: `${email}`,
subject:"Запрос на восстановление пароля",
text:`Вы отправили запрос на восстановление пароля,
перейдите по ссылке плз:{url}`,
html:`Вы отправили запрос на восстановление пароля,
перейдите по ссылке плз: <br><a> href="${url}">${url}</a>`});
return res.send({message:'Email successffuly send'})
} catch (e){
console.log(e)
res.status(502).send({message:'mail got stuck in ', e })
}
})
/**reset token in password recovery */
router.get('/', async(req: Request, res: Response):Promise<Response|void>=>{
const token = req.query.token;
if(!token) return res.status(401).send({Message:'token not exists'})
const passwordRecovery = await dataSource
.createQueryBuilder()
.from(PasswordRecovery,'passwordRecovery')
.select('passwordRecovery')
.innerJoinAndSelect('passwordRecovery.user', 'user')
.where(' passwordRecovery.token=:token',{token})
.getOne()
if(!passwordRecovery || !passwordRecovery.enabled) return res.status(404).send({message:"Token is not valid"})
res.send(passwordRecovery)
passwordRecovery.enabled=false;
try{
await passwordRecovery.save();
} catch(e){
console.log(e)
}
})
/**change password */
router.patch('/:id/change-password', async (req: Request, res: Response):Promise<Response|void>=>{
const user = await dataSource
.getRepository(User)
.findOneBy({id:req.params.id})
if(!user) return res.status(404).send({Message:'user not found'})
const salt = await bcrypt.genSalt(SALT_WORK_FACTOR);
let newPassword:string = await bcrypt.hash(req.body.password, salt);
user.password = newPassword
try{
await user.save()
res.send({message:"Password saved"})
} catch (e){
res.status(502).send({message:"error in saving new psasword"})
}
})
export default router;
\ No newline at end of file
...@@ -5,6 +5,7 @@ import tasks from './routers/tasks'; ...@@ -5,6 +5,7 @@ import tasks from './routers/tasks';
import projects from './routers/projects'; import projects from './routers/projects';
import {myDataSource} from './app-data-source'; import {myDataSource} from './app-data-source';
import copyTasks from './routers/copyTasks'; import copyTasks from './routers/copyTasks';
import passwordRecovery from './routers/passwordRecovery';
myDataSource myDataSource
...@@ -25,6 +26,7 @@ app.use('/users',users) ...@@ -25,6 +26,7 @@ app.use('/users',users)
app.use('/tasks',tasks) app.use('/tasks',tasks)
app.use('/copy-tasks',copyTasks) app.use('/copy-tasks',copyTasks)
app.use('/projects',projects) app.use('/projects',projects)
app.use('/password-recovery',passwordRecovery)
const run = async() => { const run = async() => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment