Sessions
Server
Express

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

    Official example