Create a fullstack book app: Authentication and DB models
Tags: webdev, nextjs, trpc, devjournal
Published: 2022-07-11 12:51:59

Hello! In this part of the series, we'll dive into authentication and how I designed the database schema. Initially, I wanted to do just authentication, but when adding a user model with Prisma I realized it would be wise to create all models, so I get a bird's eye view of the whole DB.

## Building schema
I'll be using Prisma for working with DB. But Andi, what is Prisma? Glad you asked. It is a database ORM, which is a layer of abstraction, that helps you with DB actions, like searching in DB or creating new records. On top of that, it has full type safety.

`create-t3-app` does a lot of initializing for you. You'll have a folder called `prisma` at the root of your project, which will have `schema.prisma` file containing this 

```
generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "sqlite"
    url      = "file:./db.sqlite"
    // url      = env("DATABASE_URL")
}
```

This essentially sets your DB of type `sqlite`. Now, every time there's change in the schema, to see changes reflected in DB we have to run either
```
npx prisma db push
``` 
or 
```
npx prisma migrate dev
```
I was using mainly the first one, because I was quickly prototyping. When running `push`, the DB will be wiped. To keep changes you'll want to run the latter.

So, as I wrote in the last post the DB will consist of users, which will have book-notes assigned to them, and book-notes will have chapters assigned to them. Here are the initial models

```
model User {
    id String @id @default(uuid())
}

model BookNote {
    id String @id @default(uuid())
    isbn13 String
}

model Chapter {
    id String @id @default(uuid())
}
```

Each model has to have an identifier. You can set default value with `@default(uuid())`. ~~I didn't do it for the BookNote model, cause a  booknote identifier will be `isbn13` code of the book.~~ That will be received via the external endpoint [https://api.itbook.store/](https://api.itbook.store/). 
EDIT: yes, the `isbn13` code will be received from external endpoint, but we have to set `id` to BookNote model, as well. Otherwise, we'll have one bookNote for one book.

Next up, I added more attributes that either made sense or I could retrieve from the endpoint mentioned above.

```
model User {
    id String @id @default(uuid())

    name String @unique
    password String
}

model BookNote {
    isbn13 String @id 

    title String
    subtitle String?
    image String
    price String

    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}

model Chapter {
    id String @id @default(uuid())
    
    title String
    text String
    
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}
```

Here are some special attributes like `@unique` which make the column unique (so there are no two names identical). Then, there is `@default(now())`, which adds time to `createdAt` column, and `@updatedAt` which updates time, whenever we make a change.

Now, as I wrote earlier, each user will have multiple book-notes. Each book-note will have multiple chapters. This leads us to using one-to-many relation. If a book-note would be shareable, i.e. a book-note can have multiple users, I would use many-to-many relation. 

Looking at the docs of [one-to-many](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/one-to-many-relations) relation (sidenote: don't be scared to look at docs, memorizing is a waste of everybody's time), we get final schema

```
model User {
    id String @id @default(uuid())
    name String @unique
    password String
    bookNotes BookNote[]
}

model BookNote {
    isbn13 String @id 
    title String
    subtitle String?
    image String
    price String
    author User @relation(fields: [authorId], references: [id])
    authorId String
    chapters Chapter[]
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}

model Chapter {
    id String @id @default(uuid())
    
    title String
    text String
    bookNote BookNote @relation(fields: [bookNoteId], references: [isbn13])
    bookNoteId String
    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}
```

## JWT Authentication with trpc
First things first... what is JWT? It's a shortcut for `jsonwebtoken`. It helps you take some data, like name, email, and essentially hash it and save it to help with identifying user, by unhashing it and checking the data. This token can be saved on the client, which, from what I read, can be dangerous, or can be saved on the server (we'll do that).

Let's install some packages
```
npm i cookie jose bcrypt --save
npm i @types/cookie @types/jose @types/bcrypt --save-dev
```
I like to make checklist of everything that has to be done: 

- on the sign-in, set the user's JWT token in the request's cookie
- verify the given JWT token
- get hashed values out of the JWT token
- on the sign-out, expire user's JWT token in requests cookie

I like to first have an interface, so I can easily test stuff. Next.js has file-based routing, which means, when you create a file like `example.tsx` in the pages directory, you already have a route `/example`. Let's create `sign-up.tsx` and `login.tsx` files. Both will follow structure

```
function FormPage() {
  create a function that will send auth data to server and redirect to particular page

  return (
    // render a form that calls function mentioned above
  );
}

export default FormPage;

```
I don't want to go to specifics, the main idea is written above. We can refactor to infinity, but that's not important... right now, I just want an interface.

### Register

The whole Backend will be written thanks to `tRPC`. It is a technology, that lets you define functions on the Backend and call them on the Frontend. For creating such a function one needs a router. Let's create one in `server/router` directory, it will be called `auth.router.ts`. Here, we'll create a new router by calling `createRouter()` and chaining `mutation()` to it. The `tRPC` has 2 main types of functions `mutation` and `query`. I use `mutation`, whenever I have to fetch the data on some event, like on click. And I use `query`, whenever I just have to fetch stuff in the component. We'll use mutation for sign-up, because we're sending data on click.

```
export const authRouter = createRouter()
  .mutation("signup", {
    input: z.object({
      name: z.string(),
      password: z.string(),
    }),
    async resolve({ input, ctx }) {
      const { prisma } = ctx;

      const { name, password } = input;

      
      const user = await prisma.user.create({
        data: {
          name: name,
          password: bcrypt.hashSync(password, 10),
        },
      });

      return { name: user.name };
    },
  })
```
The function expects you to send some input, in above-defined format, then creates a new user with `Prisma`. On Frontend we'll create mutation via
```
const mutation = trpc.useMutation(["auth.sign-up"]);
``` 
tRPC is a layer above React-Query, so it works like `useMutation` there.

### Login

Let's chain another function `login`.

```
.mutation("login", {
    input: z.object({ name: z.string(), password: z.string() }),
    async resolve({ ctx, input }) {
      const { prisma } = ctx;

      const { name, password } = input;

      const user = await prisma.user.findUnique({
        where: {
          name: name,
        },
      });

      if (bcrypt.compareSync(password, user.password)) {
        const token = await setUserCookie(user.name, ctx.res);
        return { name: user.name, token };
      } 
    },
  })
```
Login takes some input and searches the DB to find the unique name (that's why @unique is in the schema). Finding users is not enough though, we have to compare passwords. If they're the same we take the name and hash it with JWT and set in on the server. Create a new file `auth.ts`.

```
// src/lib/auth.s

const SECRET = process.env.JWT_SECRET;

export async function setUserCookie(name: string, res: NextApiResponse) {
  try {
    const token = await new SignJWT({})
      .setProtectedHeader({ alg: "HS256" })
      .setJti(name)
      .setIssuedAt()
      .setExpirationTime("2h")
      .sign(new TextEncoder().encode(SECRET));

    res.setHeader(
      "Set-cookie",
      cookie.serialize("token", token, {
        httpOnly: true,
        path: "/",
        maxAge: 60 * 60 * 2, // 2 hours in seconds,
      })
    );

    return token;
  } catch (e) {
    console.error({ setCookies: e });
  }
}

``` 

The signing part is a little scary with all the chaining, but I literally just copied it from their [docs](https://github.com/panva/jose/blob/main/docs/classes/jwt_sign.SignJWT.md#readme). In function, we're setting the header with `setHeader` (method of Next API Response). Thanks to package `cookie` we can serialize the hashed token to header cookies, here we're setting `httpOnly: true`, which makes it so we have cookies on the server and not the client, `path` these cookies exists on, and maximum age of the cookie.

You can see there's also a `SECRET` variable. It is a key that hashes the data. We'll be using this value at verifying the given token, speaking of... let's do that

```
// src/lib/auth.s

interface UserJwtPayload {
  jti: string;
  iat: number;
}

export async function verifyJWT(token: string) {
  const authSession = await jwtVerify(token, new TextEncoder().encode(SECRET));
  return authSession.payload as UserJwtPayload;
}
```
We'll get some payload from the hashed token. The payload will be of type UserJwtPayload, and the data, we hashed, will be in `jti` attribute.

### Logout

At last, we want to expire token, when user logs out. For that, let's chain new method in `auth.router.ts`.

```
 .mutation("logout", {
    async resolve({ ctx }) {
      await expireUserCookie(ctx.res);

      return true;
    },
  })
```

```
// src/lib/auth.s

export function expireUserCookie(res: NextApiResponse) {
  res.setHeader(
    "set-cookie",
    cookie.serialize("token", "invalid", {
      httpOnly: true,
      path: "/",
      maxAge: 0,
    })
  );
}
```

### tRPC Context

You may wonder, what is the `ctx` variable in `resolve`?  The `tRPC` comes with the handy thing called `context`, which runs on every request (that's the `ctx` in router resolve). We can pass the user there, so, we have it at our disposal. You can do that for any other data you feel have to be globally available for each request. If we remove user data from the server cookie, the context will pass `null` as user data.

```
const getUserFromCookies = async (req: NextApiRequest) => {
  // get JWT `token` on cookies
  const token = req.cookies["token"] || "";

  try {
    // if token is invalid, `verify` will throw an error
    const payload = await verifyJWT(token).catch((err) => {
      console.error(err.message);
    });

    if (!payload) return null;

    // find user in database
    const user = await prisma.user.findUnique({
      where: {
        name: payload.jti,
      },
    });

    return user;
  } catch (e) {
    return null;
  }
};

export const createContext = async ({
  req,
  res,
}: trpcNext.CreateNextContextOptions) => {
  const user = await getUserFromCookies(req);

  return {
    req,
    res,
    prisma,
    user,
  };
};
```

Logout can be tested with a simple button. However, it would be nice to know if the user is logged in on the client-side. Having something like context from tRPC on the client, you know... Oh wait, we have just the thing, `React Context`.

## Authentication Context
Context can be a tricky thing with (what sometimes feels like) random re-renders and such. Thanks to this [article](https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context) on developerway, created by Nadia Makarevich, I view context differently and use it with more confidence. I really recommend reading it, but long story short, people put a lot of data into the context's state. What you want to do, is split the state to API part, where you call dispatch type of functions and part with some values, like user name, email, and such. Then you create the context for each of these values, so when one changes, this change does not trigger unnecessary rerenders. It will look like this 
```
const AuthAPIContext = createContext({
  logoutUser: () => {},
  loginUser: () => {},
});

const AuthUserContext = createContext({
  user: initialState.user,
});

const reducer = (state: TState, action: TAction): TState => {
  switch (action.type) {
    case "AUTH/LOGIN":
      return {
        ...state,
        user: action.payload?.name,
      };
    case "AUTH/LOGOUT":
      return {
        ...state,
        user: null,
      };
    default:
      return state;
  }
};

export const AuthProvider = ({ children }) => {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  const api = React.useMemo(
    () => ({
      loginUser: (s: string) => dispatch(loginUser(s)),
      logoutUser: () => dispatch(logoutUser()),
    }),
    []
  );

  return (
    <AuthUserContext.Provider value={{ user: state.user }}>
      <AuthAPIContext.Provider value={api}>{children}</AuthAPIContext.Provider>
    </AuthUserContext.Provider>
  );
};

export const useAuthAPI = () => useContext(AuthAPIContext);
export const useAuthUser = () => useContext(AuthUserContext);
```


## Protecting routes
Cool! We have the login system, we have an auth context, but we're still missing some protection for routes. For example, we don't want an unauthenticated user to be looking at some parts of the website. We can do this multiple ways, however, I found two that I personally like the best, HOC and middleware.

### Higher Order Component solution
With this solution, we'll create HOC wrapping `getServerSideProps`. When there's a user logged in, we can redirect the user by returning 
```
redirect: {
  permanent: false,
  destination: "/login",
}
```
Here's the definition 
```
export const withAuth =
  (getServerSidePropsFn) =>
  async (ctx) => {
    const token = ctx.req.cookies?.token;

    if (!token) {
      return {
        redirect: {
          permanent: false,
          destination: "/login",
        },
      };
    }

    // if token is invalid, `verify` will throw an error
    const payload = await verifyJWT(token).catch((err) => {
      console.error(err.message);
    });

    if (!payload) {
      return {
        redirect: {
          permanent: false,
          destination: "/login",
        },
      };
    }

    return getServerSidePropsFn(ctx);
  };
```
And here's how I used it 
```
export const getServerSideProps = withAuth(async (
  ctx
) => {
  return {
    props: {
      ...
    },
  };
});

```
I like this solution, because redirect happens on the server, instead of the client, therefore no flashes, but we have to do this for each and every protected page. Luckily with the newest version of Next.js we might have a solution in form of middleware.

### Middleware solution
In `Next 12.2` they released middleware. From Next.js [docs](https://nextjs.org/docs/advanced-features/middleware): 

> Middleware allows you to run code before a request is completed, then based on the incoming request, you can modify the response by rewriting, redirecting, adding headers, or setting cookies.

Which is the exact thing we want, we run this middleware, **before** a request is completed where, we'll check request cookies and if there's something wrong, we redirect the user. It can even run on just particular pages with
```
export const config = {
  matcher: here you write a path or array of paths you want to match,
};
```
Here's how I used it 
```
export const config = {
  matcher: ["/my-notes", "/books/:path*"],
};

export async function middleware(req: NextRequest) {
  const verifiedToken = await verifyAuth(req).catch((err) => {
    console.error(err.message);
  });

  // redirect if the token is invalid
  if (!verifiedToken) {
    return NextResponse.redirect(new URL("/login", req.url));
  }

  return NextResponse.next();
}
```
## Conclusion
Well, that was a lot. But to conclude, we created the whole schema for our DB. Then, we created a whole login system with `tRPC` router, JWT tokens, client and server context and we finished up with protecting our routes. 

You can look at the project here [https://github.com/Attanox/it-notes](https://github.com/Attanox/it-notes)

Thanks a lot for reading! The next part will be about CRUD operations. See you there! 😉