Roll Your Own Auth
SessionsServerExpress.jsORMs

Prisma

Express.js sessions with Prisma and SQLite

Use the CLI

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

Official example

On this page