trying to setup password recovery in nodejs using nodemailer - javascript

tried to setup a password recovery in my nodejs project using nodemailer.I have got 'no recipients defined' error. how to fix the code?.
here is my code:
app.post('/forgotpass', (req, res, next)=> {
let recoveryPassword = '';
async.waterfall([
(done) => {
crypto.randomBytes(20, (err , buf) => {
let token = buf.toString('hex');
done(err, token);
});
},
(token, done) => {
User.findOne({username : req.body.username})
.then(user => {
if(!user) {
console.log("user does not exists");
return res.redirect('/forgotpass');
}
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 1800000; // 1/2 hours
user.save(err => {
done(err, token, user);
});
});
},
(token, user) => {
let smtpTransport = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 465,
auth: {
user: 'test#gmail.com',
pass: 'testpass'
}
});
let mailOptions = {
to: user.email,
from : 'Ghulam Abbas myapkforest#gmail.com',
subject : 'Recovery Email from Auth Project',
text : 'Please click the following link to recover your passoword: \n\n'+
'http://'+ req.headers.host +'/reset/'+token+'\n\n'+
'If you did not request this, please ignore this email.'
};
smtpTransport.sendMail(mailOptions, err=> {
console.log(err);
res.redirect('/forgotpass');
});
}
], err => {
if(err) res.redirect('/forgotpass');
if (err )console.log(err);
});
});
and here is the error I got :
Error: No recipients defined
at SMTPConnection._formatError (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-connection\index.js:784:19)
at SMTPConnection._setEnvelope (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-connection\index.js:995:34)
at SMTPConnection.send (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-connection\index.js:615:14)
at sendMessage (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-transport\index.js:227:28)
at C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-transport\index.js:285:25
at SMTPConnection._actionAUTHComplete (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-connection\index.js:1537:9)
at SMTPConnection.<anonymous> (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-connection\index.js:550:26)
at SMTPConnection._processResponse (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-connection\index.js:942:20)
at SMTPConnection._onData (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-connection\index.js:749:14)
at TLSSocket.SMTPConnection._onSocketData (C:\Users\JITHENDRA\Desktop\project\node\Secrets-Starting-Code\node_modules\nodemailer\lib\smtp-connection\index.js:195:44)
at TLSSocket.emit (events.js:310:20)
at addChunk (_stream_readable.js:286:12)
at readableAddChunk (_stream_readable.js:268:9)
at TLSSocket.Readable.push (_stream_readable.js:209:10)
at TLSWrap.onStreamRead (internal/stream_base_commons.js:186:23) {
code: 'EENVELOPE',
command: 'API'
}
that's the error I have got.please fix the code
I just want the node mailer to send the mail after verifying the user exists or not

This error means that you aren't passing a mailOptions.to value, so the user.email value is empty or not a valid email address. If you utilize async/await you can write the code more concisely and avoid using async.waterfall to make it easier to debug. There are a few changes/typos noted below -
// define transport here, outside the handler
const smtpTransport = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 465,
auth: {
user: 'test#gmail.com',
pass: 'testpass'
}
});
// use async function as the handler
app.post('/forgotpass', async (req, res) => {
try {
const user = await User.findOne({username : req.body.username});
if (!user) {
console.log("user does not exists");
return res.redirect('/forgotpass');
}
// make sure you have a valid email here
console.log(`Sending email to ${user.email}`);
// get token here once we have a valid user
const token = crypto.randomBytes(20);
// update the user with the token
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 1800000; // 1/2 hours
await user.save();
// send the email
await smtpTransport.sendMail({
to: user.email,
from : 'Ghulam Abbas <myapkforest#gmail.com>', // <-- wrap email address in <>
subject : 'Recovery Email from Auth Project',
text : 'Please click the following link to recover your passoword: \n\n'+ // <-- typo: "passoword" should be "password"
'http://'+ req.headers.host +'/reset/'+token+'\n\n'+ // <-- use HTTPS!
'If you did not request this, please ignore this email.'
});
} catch (err) {
// catch all exceptions here...
console.log(err);
}
// everything redirects to the GET handler for forgotpass
return res.redirect('/forgotpass');
});

Related

Express app error: [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

const router = require("express").Router();
const mongoose = require("mongoose");
const User = require("../models/Users");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
// Route 1: create new user at /api/createuser
router.post("/createuser", async (req, res) => {
try {
console.log(req.body);
const salt = await bcrypt.genSaltSync(10);
const hash = await bcrypt.hashSync(req.body.password, salt);
password = hash;
const user = new User({
name: req.body.name,
email: req.body.email,
password: password,
});
user
.save()
.then(() => {
res.json({ message: "User created successfully" });
})
.catch((err) => {
res.json({ message: "Error: " + err });
});
console.log(password);
} catch (err) {
res.json({ message: err });
}
});
// Route 2: Login user at /api/login
router.post("/login", async (req, res) => {
try {
console.log("login endpoint triggered");
const user = await User.findOne({ email: req.body.email });
if (!user) {
res.json({ message: "User does not exist" });
}
const passwordIsValid = await bcrypt.compare(
req.body.password,
user.password
);
if (!passwordIsValid) {
res.json({ message: "Invalid password" });
} else {
const data = {
id: user._id,
};
const token = await jwt.sign(data, process.env.SECRET);
res.json(token);
}
} catch (error) {
res.json({ message: error });
}
});
module.exports = router;
Whenever I am testing the login endpoint, my app crashes if i try to put incorrect password or unregistered email.
It says Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
I have only sent one response to the client even then it is showing this error.
In the terminal, it is showing error at catch block of login endpoint.
Can anyone look into it and tell me why am i getting this error.
Make sure that when the response is an error, the rest of the middleware is not executed any more. For example, put a return before the res.json statement:
if (!user) {
return res.json({ message: "User does not exist" });
}
Otherwise, the "Invalid password" json might get issued after this one, but two res.jsons in one response lead to the observed error.

send email nodemailer without using gmail smtp

I have a business email using bell.net email address, and on their site, it says use smtphm.sympatico.ca, as the smtp hostname, as well as port 25 / 587... I have my nodemailer code set up like this:
app.post('/sendBatchEmail', (req, res) => {
var emails = [];
var emailSubject = req.body.emailSubject;
var emailMessage = req.body.emailMessage;
//perform db2 send
var sendEmail = "select * from testEmails"
ibmdb.open(ibmdbconnMaster, function (err, conn) {
if (err) return console.log(err);
conn.query(sendEmail, function (err, rows) {
if (err) {
console.log(err);
}
for (var i = 0; i < rows.length; i++) {
emails.push(rows[i].EMAIL)
}
//send email
async function main() {
let transporter = nodemailer.createTransport({
host: "smtphm.sympatico.ca",
port: 25,
secure: false, // true for 465, false for other ports
auth: {
user: "xxx#bell.net",
pass: "xxx",
},
});
// send mail with defined transport object
let sendBatch = await transporter.sendMail({
from: "my1email#bell.net", // sender address
to: "myemail#gmail.com",
bcc: emails, // list of receivers
subject: emailSubject, // Subject line
text: emailMessage, // plain text body
});
console.log("Message sent: %s", sendBatch.messageId);
// Message sent: <b658f8ca-6296-ccf4-8306-87d57a0b4321#example.com>
// Preview only available when sending through an Ethereal account
// console.log("Preview URL: %s", sendBatch.getTestMessageUrl);
// Preview URL: https://ethereal.email/message/WaQKMgKddxQDoou...
}
main().catch(console.error);
res.redirect("/");
conn.close(function () {
console.log("closed the function app.get(/sendEmailBatch)");
});
});
});
})
however, this doesn't work and gives me this error:
Error: Invalid login: 535 Authentication failed
at SMTPConnection._formatError (/Users/ga/Desktop/PSL/mbs/node_modules/nodemailer/lib/smtp-connection/index.js:774:19)
at SMTPConnection._actionAUTHComplete (/Users/ga/Desktop/PSL/mbs/node_modules/nodemailer/lib/smtp-connection/index.js:1513:34)
at SMTPConnection.<anonymous> (/Users/ga/Desktop/PSL/mbs/node_modules/nodemailer/lib/smtp-connection/index.js:540:26)
at SMTPConnection._processResponse (/Users/ga/Desktop/PSL/mmbs/node_modules/nodemailer/lib/smtp-connection/index.js:932:20)
at SMTPConnection._onData (/Users/ga/Desktop/PSL/mbs/node_modules/nodemailer/lib/smtp-connection/index.js:739:14)
at TLSSocket.SMTPConnection._onSocketData (/Users/ga/Desktop/PSL/mbs/node_modules/nodemailer/lib/smtp-connection/index.js:189:44)
at TLSSocket.emit (events.js:315:20)
at addChunk (internal/streams/readable.js:309:12)
at readableAddChunk (internal/streams/readable.js:284:9)
at TLSSocket.Readable.push (internal/streams/readable.js:223:10) {
code: 'EAUTH',
response: '535 Authentication failed',
responseCode: 535,
command: 'AUTH PLAIN'
}
but works when I change it back to gmail... any ideas?

API endpoint to execute send emails using the credentials previous stored in the database. (Using Gmail REST API)

I've got an assignment, and they have asked me to obtain user credentials using OAuth 2.0 and save it in a database (say MongoDB). I was able to complete this step.
The second task is to have an API endpoint to execute send emails using the credentials previous stored. (Using Gmail REST API). I am struggling with this second task and have been searching all over the internet.
PS: I'm doing it in Nodejs
Documentation : https://developers.google.com/gmail/api/quickstart/nodejs
Setup Gmail REST API and get your clientId and client secret
write a function to send email
i have used nodemailar in example, should be similar to gmail
var nodemailer = require('nodemailer');
var crypto = require('crypto');
let transporter = nodemailer.createTransport({
host: 'smtp.sendgrid.net',
port: 465,
auth: {
user: 'apikey',
pass: 'Your Key'
}
});
function generateVerificationToken() {
return crypto.randomBytes(16).toString('hex');
}
exports.addUserEmail = function (req, res) {
Users.find({ email: req.body.email }, (err, users) => {
if (err) { res.send("Err"); return }
if (users.length == 0) {
var user = new Users(req.body);
user.verification.verification_token = generateVerificationToken();
user.save(function (err, email) {
if (err)
res.send(err);
sendVerificationEmail(req, user.verification.verification_token, email.email, (err) => {
if (err) { console.log(err); return res.status(500).send({ msg: err.message }); }
res.status(200).send({
'A verification email has been sent to ' : email.email
});
});
});
}
else {
res.status(400).send({ Error : 'Invalid Request',
Error : 'This email is already Registered'
});
}
});
};
function sendVerificationEmail(req, token, email, cb) {
let emailText = 'Hello,\n\n' + 'Please verify your account by clicking the link: \nhttp:\/\/' + req.headers.host + '\/user\/verify\/?token=' + token + '&email=' + email + '.\n';
var mailOptions = {
from: 'donotreply#Express-Server.com',
to: email,
subject: 'Account Verification Token',
text: emailText
};
transporter.sendMail(mailOptions, function (err) {
cb(err);
});
}
In your route
app.route('/user/register')
.post(users.addUserEmail);

Using Express and MongoDB - How do I log out a User?

I was following a tutorial at Authentication in NodeJS With Express and Mongo - CodeLab #1
I got everything to work perfectly, but the tutorial does not address how to log out a user.
From what I can tell, the session is being saved on Mongoose Atlas, which is the database I am using. When I log a user in with Postman, I get a token back. But I am not sure how to configure the /logout route.
Here is my code:
//routes/user.js
const express = require("express");
const { check, validationResult } = require("express-validator");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const router = express.Router();
const auth = require("../middleware/auth");
const User = require("../models/User");
/**
* #method - POST
* #param - /signup
* #description - User SignUp
*/
//Signup
router.post(
"/signup",
[
check("username", "Please Enter a Valid Username")
.not()
.isEmpty(),
check("email", "Please enter a valid email").isEmail(),
check("password", "Please enter a valid password").isLength({
min: 6
})
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
const {
username,
email,
password
} = req.body;
try {
let user = await User.findOne({
email
});
if (user) {
return res.status(400).json({
msg: "User Already Exists"
});
}
user = new User({
username,
email,
password
});
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
"randomString", {
expiresIn: 10000
},
(err, token) => {
if (err) throw err;
res.status(200).json({
token
});
}
);
} catch (err) {
console.log(err.message);
res.status(500).send("Error in Saving");
}
}
);
// Login
router.post(
"/login",
[
check("email", "Please enter a valid email").isEmail(),
check("password", "Please enter a valid password").isLength({
min: 6
})
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
const { email, password } = req.body;
try {
let user = await User.findOne({
email
});
if (!user)
return res.status(400).json({
message: "User Not Exist"
});
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch)
return res.status(400).json({
message: "Incorrect Password !"
});
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
"randomString",
{
expiresIn: 3600
},
(err, token) => {
if (err) throw err;
res.status(200).json({
token
});
}
);
} catch (e) {
console.error(e);
res.status(500).json({
message: "Server Error"
});
}
}
);
// router.route("/logout").get(function (req, res, next) {
// if (expire(req.headers)) {
// delete req.user;
// return res.status(200).json({
// "message": "User has been successfully logged out"
// });
// } else {
// return next(new UnauthorizedAccessError("401"));
// }
// });
router.get("/me", auth, async (req, res) => {
try {
// request.user is getting fetched from Middleware after token authentication
const user = await User.findById(req.user.id);
res.json(user);
} catch (e) {
res.send({ message: "Error in Fetching user" });
}
});
router.get('/logout', isAuthenticated, function (req, res) {
console.log('User Id', req.user._id);
User.findByIdAndRemove(req.user._id, function (err) {
if (err) res.send(err);
res.json({ message: 'User Deleted!' });
})
});
module.exports = router;
function isAuthenticated(req, res, next) {
console.log("req: " + JSON.stringify(req.headers.authorization));
// if (!(req.headers && req.headers.authorization)) {
// return res.status(400).send({ message: 'You did not provide a JSON web token in the authorization header' });
//}
};
///middleware/auth.js
const jwt = require("jsonwebtoken");
module.exports = function (req, res, next) {
const token = req.header("token");
if (!token) return res.status(401).json({ message: "Auth Error" });
try {
const decoded = jwt.verify(token, "randomString");
req.user = decoded.user;
next();
} catch (e) {
console.error(e);
res.status(500).send({ message: "Invalid Token" });
}
};
///models/User.js
const mongoose = require("mongoose");
const UserSchema = mongoose.Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now()
}
});
// export model user with UserSchema
module.exports = mongoose.model("user", UserSchema);
So my question is, how can I implement a /logout route so that if the user clicks the logout button and invokes that route, their token is destroyed. I am only asking about the back-end part. I can handle using axios.
Thanks.
From what I see, you are not saving any session data or storing tokens anywhere - which is great. You are simply appending the token to your headers in requests to the API.
So the only thing you can do is possibly expire the token in the /logout route
and then ensure you delete the token on the client - could be localStorage, sessionStorage etc - your client code needs to kill the token so it cannot be included again.
Side note:
You are not extending the token lifetime anywhere, so even if the user keeps interacting on the website, the token expiration is not being updated. You will need to manually refresh the token/generate a new token to have a sliding expiration.
I would suggest you save the token in cookies rather. Set the cookie to HttpOnly, Secure, and specify the domain. This is far more secure and will allow you to also expire the cookie from the API. If any scripts you include get compromised, they can access all your users’ tokens easily.
Example:
import {serialize} from 'cookie';
import jsend from 'jsend';
...
const token = jwt.sign(
{
id: validationResult.value.id // whatever you want to add to the token, here it is the id of a user
},
privateKeyBuffer,
{
expiresIn: process.env.token_ttl,
algorithm: 'RS256'
});
const cookieOptions = {
httpOnly: true,
path: '/',
maxAge: process.env.token_ttl,
expires: new Date(Date.now() + process.env.token_ttl),
sameSite: process.env.cookie_samesite, // strict
domain: process.env.cookie_domain, // your domain
secure: process.env.cookie_secure // true
};
const tokenCookie = await serialize('token', token, cookieOptions);
res.setHeader('Set-Cookie', [tokenCookie]);
res.setHeader('Content-Type', 'application/json');
res.status(200).json(jsend.success(true));
Then in logout:
// grab from req.cookies.token and validate
const token = await extractToken(req);
// you can take action if it's invalid, but not really important
if(!token) {
...
}
// this is how we expire it - the options here must match the options you created with!
const cookieOptions = {
httpOnly: true,
path: '/',
maxAge: 0,
expires: 0,
sameSite: process.env.cookie_samesite, // strict
domain: process.env.cookie_domain, // your domain
secure: process.env.cookie_secure // true
};
// set to empty
const tokenCookie = await serialize('token', '', cookieOptions);
res.setHeader('Set-Cookie', [tokenCookie]);
res.setHeader('Content-Type', 'application/json');
res.status(200).json(jsend.success(true));
As you have used JWT the backend will always check 2 things
1. Proper token
2. If time is over for that particular (You should handle this one)
For 2nd point, if the user time is up than from frontend you may delete the token if you have stored the token in localstorage.
For logout when user clicks on logout just delete jwt from localstorage and redirect to login or other page

Nodemailer is giving error

I'm using Nodemailer for sending a forget password mail with Gmail service.I tried to reach to the same error earlier in the StackOverflow, but I couldn't find the solution. Please help me, I have no idea why it is giving error like,
"TypeError: Cannot create property 'mailer' on string 'smtpTransport'"
Here is my code below-
var nodemailer = require('nodemailer');
app.post('/forgot', function(req, res, next) {
async.waterfall([
function(done) {
crypto.randomBytes(20, function(err, buf) {
var token = buf.toString('hex');
done(err, token);
});
},
function(token, done) {
User.findOne({ email: req.body.email }, function(err, user) {
if (!user) {
req.flash('error', 'No account with that email address exists.');
return res.redirect('/forgot');
}
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
user.save(function(err) {
done(err, token, user);
});
});
},
function(token, user, done) {
console.log(token, "Token");
console.log(user, "user")
var smtpTransport = nodemailer.createTransport('SMTP', {
service: 'gmail',
auth: {
user: 'abc#gmail.com',
pass: '123456'
}
});
var mailOptions = {
to: user.email,
from: 'myproducts#mailinator.com',
subject: 'My Products Password Reset',
text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
'http://' + req.headers.host + '/reset/' + token + '\n\n' +
'If you did not request this, please ignore this email and your password will remain unchanged.\n'
};
smtpTransport.sendMail(mailOptions, function(err) {
req.flash('info', 'An e-mail has been sent to ' + user.email + ' with further instructions.');
done(err, 'done');
});
}
], function(err) {
if (err) return next(err);
res.redirect('/forgot');
});
});
And the error is something like this-
/home/cis/Desktop/myproducts/node_modules/mongodb/lib/utils.js:132
throw err;
^
TypeError: Cannot create property 'mailer' on string 'smtpTransport'
at Mail (/home/cis/Desktop/myproducts/node_modules/nodemailer/lib/mailer/index.js:45:33)
at Object.module.exports.createTransport (/home/cis/Desktop/myproducts/node_modules/nodemailer/lib/nodemailer.js:52:14)
at /home/cis/Desktop/myproducts/app.js:185:38
at nextTask (/home/cis/Desktop/myproducts/node_modules/async/dist/async.js:5310:14)
at next (/home/cis/Desktop/myproducts/node_modules/async/dist/async.js:5317:9)
at /home/cis/Desktop/myproducts/node_modules/async/dist/async.js:958:16
at /home/cis/Desktop/myproducts/app.js:177:11
at /home/cis/Desktop/myproducts/node_modules/mongoose/lib/model.js:3913:16
at model.$__save.error (/home/cis/Desktop/myproducts/node_modules/mongoose/lib/model.js:342:7)
at /home/cis/Desktop/myproducts/node_modules/kareem/index.js:297:21
at next (/home/cis/Desktop/myproducts/node_modules/kareem/index.js:209:27)
at Kareem.execPost (/home/cis/Desktop/myproducts/node_modules/kareem/index.js:217:3)
at _cb (/home/cis/Desktop/myproducts/node_modules/kareem/index.js:289:15)
at $__handleSave (/home/cis/Desktop/myproducts/node_modules/mongoose/lib/model.js:280:5)
at /home/cis/Desktop/myproducts/node_modules/mongoose/lib/model.js:208:9
at args.push (/home/cis/Desktop/myproducts/node_modules/mongodb/lib/utils.js:404:72)
[nodemon] app crashed - waiting for file changes before starting...
The Nodemailer structure has been changed, try use this :
smtpTransport = nodemailer.createTransport({
service: 'Gmail',
auth: {
xoauth2: xoauth2.createXOAuth2Generator({
user: 'youremail#gmail.com',
//and other stuff here
});
}
});
var nodemailer = require("nodemailer");
var smtpTransport = nodemailer.createTransport({
service: "Yahoo", // sets automatically host, port and connection security settings
auth: {
user: "xxxxxxxxxx95#yahoo.com",
pass: "xxxxxxxxxxxx"
}
});
function mail(messageBody) {
let messageBodyJson = JSON.stringify(messageBody)
smtpTransport.sendMail({ //email options
from: "xxxxxxxxxx95#yahoo.com", // sender address. Must be the same as authenticated user if using Gmail.
to: "xxxxxxxxxx95#gmail.com", // receiver
subject: "Emailing with nodemailer", // subject
text: messageBodyJson // body
}, function(error, response){ //callback
if(error){
console.log("error",error);
}else{
console.log(response);
}
// smtpTransport.close(); // shut down the connection pool, no more messages. Comment this line out to continue sending emails.
});
}
mail("your mail message");
Try this.
Link to a similar question
Gmail / Google app email service requires OAuth2 for authentication. PLAIN text password will require disabling security features manually on the google account.
To use OAuth2 in Nodemailer, refer: https://nodemailer.com/smtp/oauth2/
Sample code:
var email_smtp = nodemailer.createTransport({
host: "smtp.gmail.com",
auth: {
type: "OAuth2",
user: "youremail#gmail.com",
clientId: "CLIENT_ID_HERE",
clientSecret: "CLIENT_SECRET_HERE",
refreshToken: "REFRESH_TOKEN_HERE"
}
});

Categories