Çoğumuz hesap kurtarma sürecini en az bir kez deneyimlemişizdir; bir şifreyi unuttuğumuzda, yeni bir şifre oluşturmak ve sisteme yeniden erişim sağlamak için prosedürlere ihtiyaç duyulur. Bu makale, rotaları yönetmek ve gerekli işlemleri gerçekleştirmek için Express'in yanı sıra Node.js, Knex ve bazı açıklanmayan araçları kullanarak böyle bir sürecin uygulanmasına odaklanmaktadır.
Yönlendirici uygulamasını, URL parametrelerini yönetmeyi, kanıt olarak yalnızca bir e-posta veya telefon numarası mevcut olduğunda kullanıcıya ne gönderileceğini belirlemeyi, e-posta gönderimlerini yönetmeyi ve güvenlik endişelerini ele almayı ele alacağız.
Kodlamaya dalmadan önce, aynı kod tabanıyla çalıştığımızdan emin olmak istiyorum; buna genel bağlantımdan erişebilirsiniz.
Şimdi şifremi unuttum akışının şemasına bir göz atın.
Sunucu, parola sıfırlama için geçerli bir bağlantı içeren e-postaları kullanıcı posta kutusuna göndermekten sorumlu olacak ve ayrıca belirteci ve kullanıcı varlığını doğrulayacaktır.
Node.js ile e-posta hizmetini kullanmaya ve e-posta göndermeye başlamak için mevcut bağımlılıklarımıza ek olarak aşağıdaki paketleri de yüklememiz gerekiyor:
npm i --save nodemailer handlebars
Nodemailer : SMTP veya diğer aktarım mekanizmalarını kullanarak kolayca e-posta göndermeye olanak tanıyan güçlü modül.
Gidon : Gidon, JavaScript için popüler bir şablon oluşturma motorudur. Oluşturma sırasında verilerle doldurulabilecek yer tutuculara sahip şablonlar tanımlamamıza olanak tanıyacaktır.
Şimdi geçişi oluşturmamız gerekiyor, dolayısıyla benim durumumda users
tablosuna yeni bir forgot_password_token
sütunu eklemem gerekiyor:
knex migrate:make add_field_forgot_password_token -x ts
ve oluşturulan dosyada kodu ayarladım:
import type { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.string('forgot_password_token').unique(); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.dropColumn('forgot_password_token'); }); }
Kullanıcılar tablosundaki Parolamı Unuttum Belirtecinin Taşınması
ve ardından en son dosyayı taşıyın:
knex migrate:knex
Artık users
tablosuna forgot_password_token
ayarlayabiliriz.
Parola unutma ve sıfırlama mantığını ele alan denetleyicileri yönetmek için iki yol oluşturmamız gerekir. İlk rota, şifremi unuttum sürecini başlatırken, ikincisi, doğrulama için URL'de bir belirteç parametresi bekleyerek sıfırlama işlemini gerçekleştirir. Bunu uygulamak için src/routes/
dizini içinde forgotPasswordRouter.ts
adında bir dosya oluşturun ve aşağıdaki kodu ekleyin:
import { Router } from 'express'; import { forgotPasswordController } from 'src/controllers/forgotPasswordController'; import { resetPasswordController } from 'src/controllers/resetPasswordController'; export const forgotPasswordRouter = Router(); forgotPasswordRouter.post('/', forgotPasswordController); forgotPasswordRouter.post('/reset/:token', resetPasswordController);
Şifremi Unuttum Yönlendirici
E-posta gönderme ve parolayı sıfırlama mantığını iki denetleyici yönetecektir.
Müşteri şifresini unuttuğunda oturumu olmaz, bu da e-posta veya diğer güvenlik tanımlayıcıları dışında kullanıcı verilerini alamadığımız anlamına gelir. Bizim durumumuzda, şifre sıfırlama işlemini gerçekleştirmek için bir e-posta gönderiyoruz. Bu mantığı denetleyiciye yerleştireceğiz.
forgotPasswordRouter.post('/', forgotPasswordController);
'Şifrenizi mi unuttunuz?'u hatırlıyor musunuz? Giriş formunun altındaki bağlantı genellikle giriş formundaki herhangi bir müşterinin kullanıcı arayüzünde bulunur mu? Üzerine tıklamak bizi şifre sıfırlama talebinde bulunabileceğimiz bir görünüme yönlendirir. Sadece e-postamızı giriyoruz ve kontrolör gerekli tüm prosedürleri gerçekleştiriyor. Aşağıdaki kodu inceleyelim:
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; import { TokenService } from 'src/services/TokenService'; import { EmailService } from 'src/services/EmailService'; export const forgotPasswordController = async (req: Request, res: Response) => { try { const { email, }: { email: string; } = req.body; const user = await UserModel.findByEmail(email); if (user) { const token = await TokenService.sign( { id: user.id, }, { expiresIn: '1 day', } ); await user.context.update({ forgot_password_token: token }); await EmailService.sendPasswordResetEmail(email, token); } return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };
Şifremi Unuttum Denetleyicisi
Gövdeden bir e-posta alacağız ve ardından kullanıcıyı UserModel.findByEmail
kullanarak bulacağız. Kullanıcı mevcutsa TokenService.sign
kullanarak bir JWT token oluşturuyoruz ve tokenı 1 günlük süre sonuyla forgot_password_token
kullanıcısına kaydediyoruz. Daha sonra mesajı, kullanıcının şifresini değiştirebileceği bir jetonla birlikte uygun bir bağlantıyla birlikte e-postaya göndereceğiz.
E-postayı gönderebilmek için gönderici olacak yeni e-posta adresimizi oluşturmamız gerekiyor.
Yeni bir e-posta hesabı oluşturmak için Google'a gidelim ve ardından hesap oluşturulduğunda Google Hesabınızı Yönetin bağlantısına ilerleyin. Sağ üstteki avatara tıklayarak bulabilirsiniz. Ardından soldaki menüde Güvenlik öğesine tıklayın ve ardından 2 Adımlı Doğrulama'ya basın. Aşağıda Uygulama şifreleri bölümünü bulacaksınız, oka tıklayın:
Kullanılması gereken adı girin. Benim durumumda Nodemailer
ayarladım ve Create'e bastım.
Oluşturulan şifreyi kopyalayın ve .env
dosyanıza ayarlayın. İki değişkeni dosyalayacak şekilde ayarlamamız gerekiyor:
MAIL_USER="[email protected]" MAIL_PASSWORD="vyew hzek avty iwst"
Elbette info@company_name.com
gibi düzgün bir e-postaya sahip olmak için Google Workspace veya AWS Amazon WorkMail'i AWS SES veya başka herhangi bir hizmetle birlikte kurmanız gerekir. Ancak bizim durumumuzda basit bir Gmail hesabını ücretsiz kullanıyoruz.
Hazırlanan .env
dosyasıyla e-posta gönderme hizmetimizi kurmaya hazırız. Denetleyici, mesajımız için oluşturulan jeton ve alıcı e-posta adresiyle hizmeti kullanacaktır.
await EmailService.sendPasswordResetEmail(email, token);
Şimdi src/services/EmailService.ts
oluşturalım ve servisin sınıfını tanımlayalım:
export class EmailService {}
Ve şimdi ilk veri olarak ortamı nodemailer
ile kullanmam gerekiyor:
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; }
E-posta Hizmeti
Hizmetin başlatılmasına dikkat etmeliyiz. Daha önceki yazımda da yazmıştım
import { TokenService } from 'src/services/TokenService'; import { RedisService } from 'src/services/RedisService'; import { EmailService } from 'src/services/EmailService'; export const initialize = async () => { await RedisService.initialize(); TokenService.initialize(); EmailService.initialize(); };
Hizmetleri Başlatma
Şimdi EmailService
sınıfımızda başlatmayı oluşturmaya devam edelim:
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; public static initialize() { try { EmailService.transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: this.env.USER, pass: this.env.PASS, }, }); } catch (error) { console.error('Error initializing email service'); throw error; } } }
E-posta Hizmeti Başlatma
nodemailer
kitaplığı tarafından sağlanan bir yöntem olan nodemailer.createTransport()
başlatma yöntemi vardır. E-postalarımızı göndermek için kullanılacak bir taşıyıcı nesnesi oluşturur. Yöntem, taşıyıcı için yapılandırma ayrıntılarını belirttiğiniz bir seçenek nesnesini bağımsız değişken olarak kabul eder.
Google kullanıyoruz: service: 'gmail'
e-posta servis sağlayıcısını belirtir. Nodemailer, çeşitli e-posta servis sağlayıcıları için yerleşik destek sağlar ve gmail
, taşıyıcının Gmail'in SMTP sunucusuyla çalışacak şekilde yapılandırılacağını belirtir.
Kimlik doğrulama auth
doğrulaması için, e-posta servis sağlayıcısının SMTP sunucusuna erişmek için gereken kimlik bilgilerinin ayarlanması gerekir.
user
için, e-posta göndereceğimiz e-posta adresine ayarlanmalıdır ve bu şifre, Google hesabında Uygulama Şifrelerinden oluşturulmuştur.
Şimdi servisimizin son kısmını ayarlayalım:
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; import { generateAttachments } from 'src/helpers/generateAttachments'; import { generateTemplate } from 'src/helpers/generateTemplate'; import { getHost } from 'src/helpers/getHost'; dotenv.config(); export class EmailService { // ...rest code public static async sendPasswordResetEmail(email: string, token: string) { try { const host = getHost(); const template = generateTemplate<{ token: string; host: string; }>('passwordResetTemplate', { token, host }); const attachments = generateAttachments([{ name: 'email_logo' }]); const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); console.log('Message sent: %s', info.messageId); } catch (error) { console.error('Error sending email: ', error); } } }
Şifre Sıfırlama E-postası Gönder
Devam etmeden önce, müşterinin e-posta alması için uygun ana bilgisayarı belirlemek çok önemlidir. E-posta gövdesinde bir belirteçle bağlantı kurmak çok önemlidir.
import * as dotenv from 'dotenv'; import process from 'process'; dotenv.config(); export const getHost = (): string => { const isProduction = process.env.NODE_ENV === 'production'; const protocol = isProduction ? 'https' : 'http'; const port = isProduction ? '' : `:${process.env.CLIENT_PORT}`; return `${protocol}://${process.env.WEB_HOST}${port}`; };
Ev sahibini al
Şablonlar için handlebars
kullanıyorum ve bunun için src/temlates/passwordResetTemplate.hbs
içinde ilk HTML şablonumuzu oluşturmamız gerekiyor:
<!-- passwordResetTemplate.hbs --> <html lang='en'> <head> <style> a { color: #372aff; } .token { font-weight: bold; } </style> <title>Forgot Password</title> </head> <body> <p>You requested a password reset. Please use the following link to reset your password:</p> <a class='token' href="{{ host }}/reset-password/{{ token }}">Reset Password</a> <p>If you did not request a password reset, please ignore this email.</p> <img src="cid:email_logo" alt="Email Logo"/> </body> </html>
Şifre Sıfırlama Şablonu
ve artık bu şablonu yardımcıyla yeniden kullanabiliriz:
import path from 'path'; import fs from 'fs'; import handlebars from 'handlebars'; export const generateTemplate = <T>(name: string, props: T): string => { const templatePath = path.join(__dirname, '..', 'src/templates', `${name}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); const template = handlebars.compile(templateSource); return template(props); };
Şablon Yardımcısı Oluştur
E-postamızı geliştirmek için ekler bile ekleyebiliriz. Bunu yapmak için email_logo.png
dosyasını src/assets
klasörüne ekleyin. Daha sonra aşağıdaki yardımcı işlevi kullanarak bu görüntüyü e-postanın içinde oluşturabiliriz:
import path from 'path'; import { Extension } from 'src/@types/enums'; type AttachmentFile = { name: string; ext?: Extension; cid?: string; }; export const generateAttachments = (files: AttachmentFile[] = []) => files.map(file => { const ext = file.ext || Extension.png; const filename = `${file.name}.${ext}`; const imagePath = path.join(__dirname, '..', 'src/assets', filename); return { filename, path: imagePath, cid: file.cid || file.name, }; });
Ek Oluşturma Yardımcısı
Tüm bu yardımcıları topladıktan sonra aşağıdakileri kullanarak e-posta gönderebilmemiz gerekir:
const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, });
Bu yaklaşım, hizmetin farklı içeriğe sahip e-postalar göndermek için çeşitli yöntemler kullanmasını sağlayan iyi bir ölçeklenebilirlik sunar.
Şimdi yönlendiricimiz ile denetleyiciyi tetikleyip e-postayı göndermeyi deneyelim. Bunun için kullanıyorum
Konsol size mesajın gönderildiğini söyleyecektir:
Message sent: <1k96ah55-c09t-p9k2–[email protected]>
Gelen kutusunda yeni mesajları kontrol edin:
Parolayı Sıfırla bağlantısının belirteci ve ana bilgisayarı içermesi gerekir:
http://localhost:3000/reset-password/<token>
Bu mesaj geliştirme süreci ile ilgili olduğundan port 3000
burada belirtilmiştir. Bu, parola sıfırlama formlarını işlemekten sorumlu olan istemcinin aynı zamanda geliştirme ortamında çalışacağını gösterir.
Belirtecin denetleyici tarafında, o e-postayı gönderen kullanıcıyı alabileceğimiz TokenService ile doğrulanması gerekir. Belirteci kullanan yönlendiriciyi kurtaralım:
forgotPasswordRouter.post('/reset/:token', resetPasswordController);
Denetleyici, şifreyi yalnızca belirtecin geçerli olması ve süresinin dolmamış olması durumunda, bir saat olarak ayarlanan sona erme süresine göre güncelleyecektir. Bu işlevselliği uygulamak için src/controllers/
klasörüne gidin ve aşağıdaki kodu içeren resetPasswordController.ts
adında bir dosya oluşturun:
import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { TokenService } from 'src/services/TokenService'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const resetPasswordController = async (req: Request, res: Response) => { try { const token = req.params.token; if (!token) { return res.sendStatus(400); } const userData = await TokenService.verify<{ id: number }>(token); const user = await UserModel.findOneById<User>(userData.id); if (!user) { return res.sendStatus(400); } const newPassword = req.body.password; if (!newPassword) { return res.sendStatus(400); } const hashedPassword = await bcrypt.hash(newPassword, 10); await UserModel.updateById(user.id, { password: hashedPassword, passwordResetToken: null }); return res.sendStatus(200); } catch (error) { const errors = ['jwt malformed', 'TokenExpiredError', 'invalid token']; if (errors.includes(error.message)) { return res.sendStatus(400); } return res.sendStatus(500); } };
Şifre Denetleyicisini Sıfırla
Bu denetleyici, jetonu alacak, doğrulayacak, şifresi çözülmüş verilerden kullanıcı kimliğini çıkaracak, ilgili kullanıcıyı alacak, istemci tarafından istek gövdesinde gönderilen yeni şifreyi alacak ve veritabanındaki şifreyi güncellemeye devam edecektir. Sonuçta bu, müşterinin yeni şifreyi kullanarak oturum açmasını sağlar.
E-posta hizmetinin ölçeklenebilirliği, şifre güncellemesini belirten ve daha sonra oturum açmayı sağlayanlar gibi onay veya başarı mesajlarının gönderilmesi gibi çeşitli yaklaşımlarla gösterilmektedir. Ancak parolaları yönetmek, özellikle uygulama güvenliğinin artırılmasının zorunlu olduğu durumlarda büyük bir zorluktur.
Şifre değişikliklerine izin vermeden önce belirteç karşılaştırması, e-posta ve şifre doğrulama gibi ek kontroller de dahil olmak üzere güvenliği artırmak için çok sayıda seçenek mevcuttur.
Diğer bir seçenek de, sunucu tarafında doğrulama için kullanıcının e-postasına bir kodun gönderildiği bir PIN kodu sistemi uygulamaktır. Bu önlemlerin her biri, e-posta gönderme özelliklerinin kullanılmasını gerektirir.
İçinde bulabileceğiniz tüm uygulanan kodlar
Lütfen bu yapıyla ilgili deneyler yapmaktan çekinmeyin ve bu konuyla ilgili beğendiğiniz yönlere ilişkin geri bildirimlerinizi paylaşın. Çok teşekkür ederim.
Burada, bu makalede kullandığım birkaç referansı bulabilirsiniz:
Burada da yayınlandı