Skip to content

Commit

Permalink
Merge pull request #46 from erland-syafiq/auth-extensions
Browse files Browse the repository at this point in the history
Extended Auth
  • Loading branch information
erland-syafiq authored Jul 10, 2024
2 parents 933d36d + d7ad32d commit 5a9934a
Show file tree
Hide file tree
Showing 17 changed files with 381 additions and 128 deletions.
4 changes: 3 additions & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
Here are all of the APIs the VTMUNC website uses. Linked are more detailed api documentation, including example headers and responses, as well as which endpoints require authorization.

## Table of Contents
- [/applicants](/docs/api/applicants.md)
- [/api/applicants](/docs/api/applicants.md)
- [/api/login](/docs/api/login.md)
- [/api/logout](/docs/api/logout.md)
2 changes: 1 addition & 1 deletion docs/api/applicants.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# /applicants
# /api/applicants

## Base URL
`/api/applicants`
Expand Down
105 changes: 105 additions & 0 deletions docs/api/login.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# /api/login

## Base URL
`/api/login`

## Overview
The `/api/login` endpoint handles user authentication for admin access.

## Endpoints

### POST `/api/login`

#### Description
Authenticate a user with email and password to generate an encrypted token stored in a cookie.

#### Request Body
| Field | Type | Description |
|---------------|--------|--------------------------------------|
| `email` | string | The email of the user. |
| `password` | string | The password of the user. |
| `rememberMe` | bool | Extends length of login session. (Optional; Default: false) |

#### Response Status Codes
| Status Code | Description |
|-------------|---------------------------|
| 200 OK | Returns admin details. |
| 401 Unauthorized | Invalid email or password. |

#### Response Body
| Field | Type | Description |
|----------|--------|--------------------------------------|
| `username` | string | The username of the user. |
| `email`| string | The email of the user. |

#### Example Request
```http
POST /api/login HTTP/1.1
Host: yourdomain.com
Content-Type: application/json
{
"email": "[email protected]",
"password": "admin_password"
}
```

#### Example Response
On success:
```json
{
"username": "Admin",
"email": "[email protected]"
}
```

On failure:
```json
{
"message": "Failed validation"
}
```

### GET `/api/login`

#### Description
Check if user is authenticated as admin based on the presence and validity of the stored token in the cookie.

#### Headers
| Key | Value |
|---------------|------------------------|
| Authorization | Bearer `<JWT token>` |

#### Response Status Codes
| Status Code | Description |
|-------------|-----------------------------------|
| 200 OK | User authenticated as admin. |
| 401 Unauthorized | Invalid cookie. |

#### Response Body
| Field | Type | Description |
|----------|--------|--------------------------------------|
| `username` | string | The username of the user. |
| `email`| string | The email of the user. |

#### Example Request
```http
GET /api/login HTTP/1.1
Host: yourdomain.com
Authorization: Bearer <JWT token>
```

#### Example Response
On success:
```json
{
"username": "Admin",
"email": "[email protected]"
}
```

On failure:
```json
{
"message": "Failed validation"
}
31 changes: 31 additions & 0 deletions docs/api/logout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# /api/login

## Base URL
`/api/logout`

## Overview
The `/api/logout` endpoint logs out user and clears user authentication cookies.

## Endpoints

### GET `/api/logout`

#### Description
Clears user authentication token. Always succeeds and returns status code 200 with no body.

#### Headers
| Key | Value |
|---------------|------------------------|
| Authorization | Bearer `<JWT token>` |

#### Response Status Codes
| Status Code | Description |
|-------------|-----------------------------------|
| 200 OK | User logged out. |

#### Example Request
```http
GET /api/login HTTP/1.1
Host: yourdomain.com
Authorization: Bearer <JWT token>
```
43 changes: 43 additions & 0 deletions site/app/api/(auth)/login/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import { getHashedAdminPassword, isUserAdmin, encrypt } from "@/app/utils/AuthUtils";

const { ADMIN_USERNAME } = process.env;

const TOKEN_EXPIRATION_SHORT = 3 * 60 * 60 * 1000; // 3 hours
const TOKEN_EXPIRATION_LONG = 30 * 24 * 60 * 60 * 1000; // 30 days

export async function POST(request) {
try {
// Get email and password for login
const body = await request.json();
const { email, password, rememberMe = false } = body;

// Used bcrypt module to stop timing attacks
if (email !== ADMIN_USERNAME || !await bcrypt.compare(password, await getHashedAdminPassword())) {
return NextResponse.json({ message: "Unauthorized"}, { status: 401 });
}

// Create encrypted token with user email and expiration time
const expiresIn = rememberMe ? TOKEN_EXPIRATION_LONG : TOKEN_EXPIRATION_SHORT;
const expires = new Date(Date.now() + expiresIn);
const token = await encrypt({ email, expires }, expires);

// Set cookies 'vtmunc_admin' for admin access
cookies().set("vtmunc_admin", token, { expires, httpOnly: true });

return NextResponse.json({ username: "Admin", email: email}, { status: 200 });
}
catch (e) {
return NextResponse.json({ message: "Unauthorized"}, { status: 401 });
}
}

export async function GET(request) {
if (!await isUserAdmin(request)) {
return NextResponse.json({ message: "Invalid cookie"}, { status: 401 });
}

return NextResponse.json({ username: "Admin", email: ADMIN_USERNAME}, { status: 200 });
}
6 changes: 6 additions & 0 deletions site/app/api/(auth)/logout/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { cookies } from "next/headers";

export async function GET() {
cookies().delete("vtmunc_admin");
return new Response("", {status: 200});
}
45 changes: 0 additions & 45 deletions site/app/api/auth/login/route.js

This file was deleted.

4 changes: 2 additions & 2 deletions site/app/applicants/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export default function DashboardPage() {
if (applicants.length == 0) {
return (
<main className="vh-100 dashboard d-flex justify-content-center align-items-center">
<div class="spinner-grow applicantsLoading text-primary" role="status">
<span class="sr-only">Loading...</span>
<div className="spinner-grow applicantsLoading text-primary" role="status">
<span className="sr-only">Loading...</span>
</div>
</main>
)
Expand Down
84 changes: 84 additions & 0 deletions site/app/components/AuthProvider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client"

import { createContext, useContext, useEffect, useState } from "react"

const AuthContext = createContext();

export default function AuthProvider({ children }) {
const [user, setUser] = useState({});

const isAuthenticated = Object.keys(user).length > 0;

// Attemps to auto login
useEffect(() => {
async function autoLogin() {
try {
const response = await fetch("/api/login");
if (!response.ok) {
throw new Error("Invalid credentials");
}
const user = await response.json();
setUser(user);
}
catch(e) {
}
}

autoLogin();
}, [])


async function login(email, password, rememberMe = false) {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, rememberMe }),
});

if (!response.ok) {
throw new Error("Invalid email and password");
}
}

async function logout() {
try {
const response = await fetch("/api/logout");

if (!response.ok) {
throw new Error("Server error");
}

setUser({});
window.location.href = "/";
}
catch (e) {
}
}



return (
<AuthContext.Provider value={{user, isAuthenticated, login, logout}}>
{children}
</AuthContext.Provider>
)
}

/**
* Objects that can be obtained from useAuth
*
* user: {
* email: string,
* username: string
* }
*
* login: (email: string, password: string, rememberMe?: boolean) => ();
* Logs in user, doesn't automatically call setUser
* Note: throws on server failure and on invalid email and password. Use with try catch statement to get error message
*
* logout: () => ();
* Logouts out user and auto-directs to the home page
*/
export function useAuth() {
return useContext(AuthContext);
}
8 changes: 8 additions & 0 deletions site/app/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import React from 'react';
import Link from "next/link";
import './Navbar.css';
import { usePathname } from 'next/navigation';
import { useAuth } from './AuthProvider';

function Navbar() {
const path = usePathname();
const { isAuthenticated, logout } = useAuth();

// If we are on home page or register/success make navbar transparent
const isTransparent = path === '/' || path === '/register/success';
Expand All @@ -29,6 +31,12 @@ function Navbar() {
{/* <Link className="nav-link" href="/resources">Resources </Link>
<Link className="nav-link" href="/sponsors"> Sponsors </Link> */}
<Link className="nav-link" href="/register"> Register </Link>
{ isAuthenticated && (
<>
<Link className="nav-link" href="/applicants"> Dashboard </Link>
<button className="btn nav-link" onClick={() => logout()}> Logout </button>
</>
)}
</div>
</div>
</nav>
Expand Down
Loading

0 comments on commit 5a9934a

Please sign in to comment.