Use the CLI (not yet supported)
npx ryo-auth@latest add session-express-prisma
Manual Installation
- loginHandler.ts
- signupHandler.ts
- meHandler.ts
- logoutHandler.ts
- server.ts
Install dependencies
npm i dotenv express express-session cors redis passport connect-redis argon2 zod cookie
Install dev dependencies
npm i --dev @prisma/client prisma @types/passport @types/express-session @types/express @types/cookie
Setup your Prisma schema
/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id String @id @default(uuid())
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Add imports
server.ts
import { config } from "dotenv";
import express, { Request, Response } from "express";
import session from "express-session";
import cors from "cors";
import { createClient } from "redis";
import passport from "passport";
import RedisStore from "connect-redis";
import { authRouter } from "@/features/auth/routes";
Setup and load env variables
.env
# Server
NODE_ENV="development"
HOST="localhost"
SCHEME="http"
PORT="4000"
CORS_ORIGIN="http://localhost:3000,http://localhost:4000"
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# API
SESSION_COOKIE_NAME="RollYourOwnAuth"
SESSION_SECRET="21fcYrNnK1OyR+UCTSfYwYMLAAcrtTOvrmj6QEouBAA="
SESSION_MAX_AGE=86400000
server.ts
config();
Setup Redis
server.ts
const app = express();
app.set("trust proxy", 1);
const redisClient = createClient();
redisClient.connect().catch(console.error);
const redisStore = new RedisStore({
client: redisClient,
prefix: "auth:",
disableTouch: true,
});
redisClient.on("error", (error: Error) => {
logger.error(error.message);
});
redisClient.on("connect", function () {
console.log(
`Redis connected at ${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`
);
});
App uses()
server.ts
app.use(
cors({
origin: (origin, callback) => {
const origins = String(process.env.CORS_ORIGIN).split(",");
if (!origin || origins.includes(String(origin))) {
callback(null, true);
} else {
callback(new Error("Not allowed."), false);
}
},
credentials: true,
optionsSuccessStatus: 200,
})
);
app.use(
session({
name: process.env.SESSION_COOKIE_NAME,
secret: String(process.env.SESSION_SECRET),
store: redisStore,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: Number(process.env.SESSION_MAX_AGE),
sameSite: "lax",
},
resave: false,
saveUninitialized: false,
})
);
app.use(passport.initialize());
app.use(passport.session());
Add Auth router
server.ts
app.get("/", (_req: Request, res: Response) =>
res.send("Roll Your Own Auth API")
);
app.use("/auth", authRouter);
app.listen(process.env.PORT, () => {
console.log(
`🚀 Server ready at ${process.env.SCHEME}://${process.env.HOST}:${process.env.PORT}`
);
});
Auth routes
/routes/index.ts
import express from "express";
import { validate } from "@/features/auth/middlewares/validate";
import { signupSchema } from "@/features/auth/validation/signup";
import { loginSchema } from "@/features/auth/validation/login";
import { logoutHandler } from "@/features/auth/controllers/logoutHandler";
import { meHandler } from "@/features/auth/controllers/meHandler";
import { signupHandler } from "@/features/auth/controllers/signupHandler";
import { loginHandler } from "@/features/auth/controllers/loginHandler";
const authRouter = express.Router();
authRouter.post("/signup", validate(signupSchema), signupHandler);
authRouter.post("/login", validate(loginSchema), loginHandler);
authRouter.get("/logout", logoutHandler);
authRouter.get("/me", meHandler);
export { authRouter };
/login controller
/controllers/loginHandler.ts
import { Request, Response } from "express";
import argon2 from "argon2";
import { prisma } from "@/config";
import { CustomSessionData } from "@/features/auth/types";
export const loginHandler = async (req: Request, res: Response) => {
const credentials = req.body;
const user = await prisma.user.findUnique({
where: {
email: credentials.email,
},
select: {
id: true,
name: true,
email: true,
password: true,
},
});
if (!user) {
return res
.json({
message: "User not found",
})
.status(404);
}
await argon2.verify(user.password, credentials.password).catch();
(req.session as CustomSessionData).userId = String(user.id);
const userData = {
id: user.id,
name: user.name,
email: user.email,
};
return res.json(userData).status(200);
};
/signup controller
/controllers/signupHandler.ts
import { Request, Response } from "express";
import argon2 from "argon2";
import { prisma } from "@/config";
import { CustomSessionData } from "@/features/auth/types";
export const signupHandler = async (req: Request, res: Response) => {
const userDetails = req.body;
const hashedPasword = await argon2.hash(userDetails.password);
const user = await prisma.user.create({
data: {
name: userDetails.name,
email: userDetails.email,
password: hashedPasword,
},
select: {
id: true,
email: true,
name: true,
posts: true,
},
});
(req.session as CustomSessionData).userId = String(user.id);
return res.json(user).status(200);
};
/me controller
/controllers/meHandler.ts
import { Request, Response } from "express";
import { CustomSessionData } from "@/features/auth/types";
import { prisma } from "@/config";
export const meHandler = async (req: Request, res: Response) => {
const session: CustomSessionData = req.session;
const sessionId: string | undefined = session.userId;
const userId = (req?.user as any)?.id;
if (!sessionId && !userId) {
return res.json({ message: "User already logged out." }).status(400);
}
const remainingTime = session.cookie.maxAge || 0;
if (remainingTime <= 0) {
return res.json({ message: "Session has expired." }).status(401);
}
const user = await prisma.user.findUnique({
where: {
id: sessionId || userId,
},
select: {
id: true,
email: true,
name: true,
},
});
return res.json(user).status(200);
};
/logout controller
/controllers/logoutHandler.ts
import { Request, Response } from "express";
export const logoutHandler = async (req: Request, res: Response) => {
req.session.destroy((err) => {
if (process.env.SESSION_COOKIE_NAME) {
res.clearCookie(process.env.SESSION_COOKIE_NAME);
}
if (err) {
res
.json({
message: err.message,
})
.status(400);
}
res
.json({
message: "User logged out succcessfully",
})
.status(200);
});
};
⚠️
You have to install and start Redis locally