My plugin looks like
import fp from 'fastify-plugin';
import mongodb from 'fastify-mongodb';
export default fp(async (fastify) => {
fastify.register(mongodb, {
url: 'mongodb+srv://dbuser:password#cluster0.otigz.mongodb.net/myapp?retryWrites=true&w=majority',
});
});
and my handler looks like
const postJoinHandler = async (
request: any,
reply: any
): Promise<{ id: string; name: string }> => {
try {
const { username, password } = request.body;
const test = await reply.mongo.db.users.insertOne({
username,
password,
});
console.log(test);
return reply.code(201).send(username);
} catch (error) {
request.log.error(error);
return reply.send(400);
}
};
Expected it to insert the username and password into the collection named users, but it didn't? and the error is Cannot read property 'db' of undefined
I also tried
reply.mongodb.users.insertOne({...
and
const test = await request.mongodb.collection('users');
test.insertOne({
username,
password,
});
console.log(test);
and
const test = await this.mongo.db.collection('users'); //<= Object is possibly 'undefined'
Routes look like
import { FastifyPluginAsync } from 'fastify';
import { postJoinSchema, postLoginSchema } from '../schemas/auth';
const auth: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post('/auth/join', postJoinSchema);
fastify.post('/auth/login', postLoginSchema);
};
export default auth;
The mongo decorator is attached to the fastify instance, not to the request nor reply object.
You should move your handlers into the routes file and read for fastify.mongo or use a named function as the handler.
In the latter case, the handler has this bounded to the fastify instance.
async function postJoinHandler (
request,
reply
) {
try {
const { username, password } = request.body;
const test = await this.mongo.db.users.insertOne({
username,
password,
});
console.log(test);
reply.code(201)
return username
} catch (error) {
request.log.error(error);
reply.code(400);
return {}
}
};
Related
Looking for a backend dev that can simply help me implement MONGODB with nextJS and the current model I have now. I have bought https://www.devias.io admin dashboard, and just want to implement auth and database reading with it.
Just want the basic auth setup. It's already setup in the FILES just wanting to know how to configure it properly based on the devias guides
Has anyone done this before I can't find any documentation on it
It's setup with mock data at the moment
SRC/API/AUTH/index.js
import { createResourceId } from '../../utils/create-resource-id';
import { decode, JWT_EXPIRES_IN, JWT_SECRET, sign } from '../../utils/jwt';
import { wait } from '../../utils/wait';
import { users } from './data';
class AuthApi {
async signIn(request) {
const { email, password } = request;
await wait(500);
return new Promise((resolve, reject) => {
try {
// Find the user
const user = users.find((user) => user.email === email);
if (!user || (user.password !== password)) {
reject(new Error('Please check your email and password'));
return;
}
// Create the access token
const accessToken = sign({ userId: user.id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
resolve({ accessToken });
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
async signUp(request) {
const { email, name, password } = request;
await wait(1000);
return new Promise((resolve, reject) => {
try {
// Check if a user already exists
let user = users.find((user) => user.email === email);
if (user) {
reject(new Error('User already exists'));
return;
}
user = {
id: createResourceId(),
avatar: undefined,
email,
name,
password,
plan: 'Standard'
};
users.push(user);
const accessToken = sign({ userId: user.id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
resolve({ accessToken });
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
me(request) {
const { accessToken } = request;
return new Promise((resolve, reject) => {
try {
// Decode access token
const { userId } = decode(accessToken);
// Find the user
const user = users.find((user) => user.id === userId);
if (!user) {
reject(new Error('Invalid authorization token'));
return;
}
resolve({
id: user.id,
avatar: user.avatar,
email: user.email,
name: user.name,
plan: user.plan
});
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
}
export const authApi = new AuthApi();
then /SRC/API/AUTH/data.js
export const users = [
{
id: '5e86809283e28b96d2d38537',
avatar: '/assets/avatars/avatar-anika-visser.png',
email: 'demo#devias.io',
name: 'Anika Visser',
password: 'Password123!',
plan: 'Premium'
}
];
This is the documentation on it
JSON Web Token (JWT)
Most auth providers use this strategy under the hood to provide access tokens. Currently, the app doesn't cover the backend service, and this service is mocked (faked) using http client interceptors. The implementation is basic, but enough to give you a starting point.
How it was implemented
Since tokens are meant to be created on the backend server, they are built with encrypt, encode and decode utility methods because they are not meant to be used on the client. These utilities can be found in src/utils/jwt. These are for development purposes only, and you must remove (or avoid using) them.
How to use JWT Provider
The app is delivered with JWT Provider as default auth strategy. If you changed or removed it, and you want it back, simply follow these steps:
Step 1: Import the provider
Open src/pages/_app.js file, import the provider and wrap the App component with it.
// src/pages/_app.js
import { AuthConsumer, AuthProvider } from '../contexts/auth/jwt-context';
const App = (props) => {
const { Component, pageProps } = props;
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
};
Step 2: Set the hook context
Open src/hooks/use-auth.js file and replace the current context the following line:
import { AuthContext } from '../contexts/auth/jwt-context';
How to use auth
Retrieve user profile
In the example below, you can find how it can be used in any component not just the App. Should you want to use it in any other component, you'll have to import the useAuth hook and use it as needed.
// src/pages/index.js
import { useAuth } from '../hooks/use-auth';
const Page = () => {
const { user } = useAuth();
return (
<div>
Email: {user.email}
</div>
);
};
Auth methods / actions
For simplicity and space limitations, the code below is used only to exemplify, actual code can be found in the components.
// src/pages/index.js
import { useAuth } from '../hooks/use-auth';
const Page = () => {
const { login } = useAuth();
const handleLogin = () => {
// Email/username and password
login('demo#devias.io', 'Password123!');
};
s
return (
<div>
<button onClick={handleLogin}>
Login
</button>
</div>
);
};
Implemented flows
Currently, the app only covers the main flows:
Register
Login
Logout
const mongoose = require('mongoose');
const jwt = require("jsonwebtoken");
// Connect to MongoDB
mongoose.connect('mongodb://localhost/yourdbname', {
useNewUrlParser: true,
useUnifiedTopology: true
});
const userSchema = new mongoose.Schema({
id: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true
},
name: {
type: String,
required: true
},
password: {
type: String,
required: true
},
plan: {
type: String,
default:
'Standard'
},
avatar: {
type: String,
default:
null
},
});
const User = mongoose.model('User', userSchema);
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '7d';
class AuthApi {
async signIn(request) {
const {
email,
password
} = request;
const user = await User.findOne({
email
});
if (!user || (user.password !== password)) {
throw new Error('Please check your email and password');
}
const accessToken = jwt.sign({
userId: user.id
}, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
return {
accessToken
};
}
async signUp(request) {
const {
email,
name,
password
} = request;
const existingUser = await User.findOne({
email
});
if (existingUser) {
throw new Error('User already exists');
}
const newUser = new User({
id: mongoose.Types.ObjectId(),
email,
name,
password,
plan: 'Standard',
avatar: null,
});
await newUser.save();
const accessToken = jwt.sign({
userId: newUser.id
}, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
return {
accessToken
};
}
async me(request) {
const {
accessToken
} = request;
const decoded = jwt.verify(accessToken, JWT_SECRET);
const {
userId
} = decoded;
const user = await User.findById(userId);
if (!user) {
throw new Error('Invalid authorization token');
}
return {
id: user.id,
avatar: user.avatar,
email: user.email,
name: user.name,
plan: user.plan
};
}
}
export const authApi = new AuthApi();
I am trying to create a login endpoint that checks to see if an email is already stored in the database. If an email exists it will return an error, otherwise it notifies that an email exists. For some reason, User.findOne({ email: req.body.email }) does not seem to work. Here is the code I am currently using (located in pages/api/login.ts.)
import dbConnect from "../../lib/dbConnect";
import User from "../../models/User"
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
await dbConnect()
//type of request
const {method} = req
if (method === "POST") {
try {
await User.findOne({email: req.body.email}, function(err, user) {
if (err) {
res.status(400).json({error: "no email found"})
}
if (user) {
res.status(200).json({success: "email found", data: user})
}
})
} catch (error) {
res.status(400).json({error: "connection error"})
}
}
}
I never seen callback with await syntax:
try {
const user = await User.findOne({ email: req.body.email });
if (user) {
res.status(200).json({success: "email found", data: user})
}
} catch (error) {
// handle error here
}
I have created an app that connects to a mongodb cluster and stores user info. The user is then able to log in with Next-Auth functionality. The app was working just fine before deploying to Vercel. On the live site I ran into some Server Config Errors. I refractored my code yet I am still running into a few errors.
I am successfully able to connect to the database for a new user sign up.
import {
connectToDatabase,
hashedPassword,
} from "../../helper/HelperFunctions";
const isEmpty = (value) => value.trim() === "";
const isTenChars = (value) => value.trim().length >= 10;
const emailValidation = (value) => {
const pattern = /^[^ ]+#[^ ]+\.[a-z]{2,3}$/;
if (value.match(pattern)) {
return true;
} else {
return false;
}
};
export default async function handler(req, res) {
if (req.method == "POST") {
let data = req.body;
const { firstName, lastName, email, password, userName } = data;
const firstNameIsValid = !isEmpty(firstName);
const lastNameisValid = !isEmpty(lastName);
const emailIsValid = emailValidation(email);
const passwordisValid = isTenChars(password);
const userNameIsValid = !isEmpty(userName);
let userDataIsValid =
firstNameIsValid &&
lastNameisValid &&
emailIsValid &&
passwordisValid &&
userNameIsValid;
if (!userDataIsValid) {
return;
}
const client = await connectToDatabase();
const db = client.db();
const existingUser = await db.collection("users").findOne({ email: email });
if (existingUser) {
res.status(422).json({ message: "User already exists, please log in!" });
console.log("User already exists, please log in!");
client.close();
return;
}
const protectedPassword = await hashedPassword(password);
await db.collection("users").insertOne({
firstName: firstName,
lastName: lastName,
email: email,
password: protectedPassword,
userName: userName,
});
client.close();
res.status(201).json({ message: "Signed up!" });
} else {
res.status(200).json({ data: req.body });
}
}
Here is my nextauth api route
import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials";
// Helper Functions
import {
connectToDatabase,
comparePasswords,
} from "../../../helper/HelperFunctions";
export default NextAuth({
session: { strategy: "jwt" },
providers: [
CredentialsProvider({
async authorize(credentials) {
const client = await connectToDatabase();
const userCollection = client.db().collection("users");
const user = await userCollection.findOne({
email: credentials.email,
});
if (!user) {
client.close();
throw new Error("No user found!");
}
const isValid = await comparePasswords(
credentials.password,
user.password
);
if (!isValid) {
client.close();
throw new Error("Invalid password");
}
client.close();
if (user) {
return {
email: user.email,
};
} else {
return null;
}
},
}),
],
});
Before I deployed my site on Vercel, this was working just fine on localhost. The user should then proceed to a new page if the result of logging in has no errors.
const result = await signIn("credentials", {
redirect: false,
email: form.email,
password: form.password,
});
if (!result.error) {
console.log(true);
router.replace("/suggestions");
} else {
console.log(result.error);
setLoginResult(result.error);
}
If you see CLIENT_FETCH_ERROR make sure you have configured the NEXTAUTH_URL environment variable.
when developing you set it to localhost:3000, now you need to set that to your deployed url.
I really need your help. My MongoDB transaction with #NestJs/mongoose not working...When My stripe payment fails rollback is not working... Still, my order collection saved the data...How can I fix this issue..?
async create(orderData: CreateOrderServiceDto): Promise<any> {
const session = await this.connection.startSession();
session.startTransaction();
try {
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save();
await this.stripeService.charge(
orderData.amount,
orderData.paymentMethodId,
orderData.stripeCustomerId,
);
await session.commitTransaction();
return order;
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
I had the same issue and i found that on github: Mongo DB Transactions With Mongoose & Nestjs
So I think, according this issue, you have to call the create method of your model, like that:
const order = await this.orderModel.create(orderData, { session });
as you can see, the Model.create method has an overload with SaveOptions as parameter:
create(docs: (AnyKeys<T> | AnyObject)[], options?: SaveOptions): Promise<HydratedDocument<T, TMethodsAndOverrides, TVirtuals>[]>;
it takes an optional SaveOptions parameter that can contain the session:
interface SaveOptions {
checkKeys?: boolean;
j?: boolean;
safe?: boolean | WriteConcern;
session?: ClientSession | null;
timestamps?: boolean;
validateBeforeSave?: boolean;
validateModifiedOnly?: boolean;
w?: number | string;
wtimeout?: number;
}
Please note that Model.save() can also take a SaveOptions parameter.
So you can also do as you did like that:
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save({ session });
A little further...
As i do many things that require a transaction, I came up with this helper to avoid many code duplication:
import { InternalServerErrorException } from "#nestjs/common"
import { Connection, ClientSession } from "mongoose"
export const mongooseTransactionHandler = async <T = any>(
method: (session: ClientSession) => Promise<T>,
onError: (error: any) => any,
connection: Connection, session?: ClientSession
): Promise<T> => {
const isSessionFurnished = session === undefined ? false : true
if (isSessionFurnished === false) {
session = await connection.startSession()
session.startTransaction()
}
let error
let result: T
try {
result = await method(session)
if (isSessionFurnished === false) {
await session.commitTransaction()
}
} catch (err) {
error = err
if (isSessionFurnished === false) {
await session.abortTransaction()
}
} finally {
if (isSessionFurnished === false) {
await session.endSession()
}
if (error) {
onError(error)
}
return result
}
}
Details
the optional parameter session is in case you are doing nested nested transaction.
that's why i check if the session is provided. If it is, it means we are in a nested transaction. So we'll let the main transaction commit, abort and end the session.
Example
for example: you delete a User model, and then the user's avatar which is a File model.
/** UserService **/
async deleteById(id: string): Promise<void> {
const transactionHandlerMethod = async (session: ClientSession): Promise<void> => {
const user = await this.userModel.findOneAndDelete(id, { session })
await this.fileService.deleteById(user.avatar._id.toString(), session)
}
const onError = (error: any) => {
throw error
}
await mongooseTransactionHandler<void>(
transactionHandlerMethod,
onError,
this.connection
)
}
/** FileService **/
async deleteById(id: string, session?: ClientSession): Promise<void> {
const transactionHandlerMethod = async (session: ClientSession): Promise<void> => {
await this.fileModel.findOneAndRemove(id, { session })
}
const onError = (error: any) => {
throw error
}
await mongooseTransactionHandler<void>(
transactionHandlerMethod,
onError,
this.connection,
session
)
}
So, in short:
You can use it like this:
async create(orderData: CreateOrderServiceDto): Promise<any> {
const transactionHandlerMethod = async (session: ClientSession): Promise<Order> => {
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save({ session });
await this.stripeService.charge(
orderData.amount,
orderData.paymentMethodId,
orderData.stripeCustomerId,
);
return order
}
const onError = (error: any): void => {
throw error
}
const order = await mongooseTransactionHandler<Order>(
transactionHandlerMethod,
onError,
this.connection
)
return order
}
Hope it'll help.
EDIT
Do not abuse of the model.save({ session }) of the same model in nested transcations.
For some reasons it will throw an error the model is updated too many times.
To avoid that, prefer using model embeded methods that update and return a new instance of your model (model.findOneAndUpdate for example).
I have the following code.
I want to implement authentication using MongoDB, mongoose, express using typescript.
I am having a typescript issue. I tried declaring type (maybe incorrectly) for findUser and did not resolve. Any tips?
model.ts
import mongoose, { Schema, Document } from 'mongoose';
import bcrypt from 'bcrypt';
export interface IUser extends Document {
username: string;
password: string;
}
const userSchema: Schema = new Schema({
username: {
type: String,
unique: true,
required: true,
},
password: {
type: String,
required: true,
},
});
// tslint:disable-next-line: only-arrow-functions
userSchema.statics.findUser = async function (username, password) {
const user = await User.findOne({ username });
if (!user) {
return;
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return;
}
return user;
};
userSchema.pre<IUser>('save', async function (next) {
const user = this;
if (user.isModified('password')) {
user.password = await bcrypt.hash(user.password, 8);
}
next();
});
const User = mongoose.model<IUser & Document>('User', userSchema);
export default User;
auth.ts (route)
ERROR:Property 'findUser' does not exist on type 'Model<IUser & Document>'.ts(2339)
import express from 'express';
import User from '../models/user-model';
const router = express.Router();
declare module 'express-session' {
// tslint:disable-next-line: interface-name
export interface SessionData {
user: { [key: string]: any };
}
}
router.post('/signin', async (req, res) => {
const { email, password } = req.body;
const user = await User.findUser(email, password);
if (user) {
req.session.user = user._id;
res.json({
message: 'You are successfully login',
auth: true,
});
} else {
res.json({
message: 'Unable to login',
auth: false,
});
}
});
export = router;
You can set a second generic on the mongoose.model() method which describes the model itself.
Here we include all of the properties of Model<IUser> and also add your custom function.
type UserModel = Model<IUser> & {
findUser: (username: string, password: string) => Promise<IUser | undefined>;
}
IUser determines the type for the documents in this model, while UserModel determines the type for the model.
const User = mongoose.model<IUser, UserModel>('User', userSchema);
Now the type for the method is known. user here gets type IUser | undefined;
const user = await User.findUser('joe', 'abcd');
Typescript Playground Link