Skip to content

Commit

Permalink
Merge pull request #33 from COSC-499-W2023/gh-29-next-auth
Browse files Browse the repository at this point in the history
feat(auth): implemented NextAuth with dummy provider
  • Loading branch information
connordoman authored Oct 30, 2023
2 parents ee669c0 + f4d8730 commit f693330
Show file tree
Hide file tree
Showing 21 changed files with 1,285 additions and 999 deletions.
29 changes: 29 additions & 0 deletions app/front-end/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,35 @@ To serve images, fonts, or downloadable files we must place them in the `./publi

To have common scripts across pages and components, we must place them in the `./src/lib` folder. This is for custom scripts that are not React components or API routes. For example, string manipulation, date formatting, etc. This makes a good place for things like database abstraction code, such as a class and its subclasses that are used to query a database. These _are not_ accessible to users from the web.

### Authentication

Components on the front end should make use of the provided `useSession()` hook from `next-auth/client` to determine if a user is logged in. This hook returns a `session` object that contains the user's information if they are logged in, or `null` if they are not. There is an example of its use in `./src/app/page.tsx`. The `status` variable is an additional convenience that takes the place of something like a `loading` state variable.

For authentication to work in your development environment, you must include a `.env.local` in the project root with the following variables:

```dotenv
# this would be the canonical url on the web when deployed
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=secret
```

Where `NEXTAUTH_SECRET` is a random string of characters.

If a new key needs to be generated, run the following command:

```bash
openssl rand -base64 32
```

During development and testing, the following credentials will be used to log in:

```json
{
"email": "johnny@example.com",
"password": "password"
}
```

## TESTING

Next.js 13 is compatible with Jest and React Testing Library out of the box. The appropriate packages are already installed. Next.js's documentation can be found here: https://nextjs.org/docs/pages/building-your-application/optimizing/testing#jest-and-react-testing-library.
Expand Down
36 changes: 33 additions & 3 deletions app/front-end/__tests__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
exports[`Home homepage is unchanged from snapshot 1`] = `
<div>
<main
class="center-column"
class="column"
>
<div
class="masthead"
>
<h1>
<h1
class="heading"
>
PrivacyPal
</h1>
<h2>
<h2
class="subheading"
>
COSC 499 Capstone Team 1
</h2>
</div>
Expand All @@ -28,6 +32,32 @@ exports[`Home homepage is unchanged from snapshot 1`] = `
>
Patternfly Button
</button>
<h2
class="subheading"
>
Test Login
</h2>
<p>
You can login with the username "
<code>
johnny@example.com
</code>
" and password "
<code>
password
</code>
"
</p>
<br />
<p>
You can visit
<a
href="/api/session"
>
this link
</a>
to see a breakdown of your current session.
</p>
</main>
</div>
`;
35 changes: 35 additions & 0 deletions app/front-end/__tests__/config.lib.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Created on Sun Oct 29 2023
* Author: Connor Doman
*/

import { extractConfigFile } from "@lib/config";

describe("Config Library Tests", () => {
it("extractConfigFile works for valid config file", async () => {
const config = await extractConfigFile("extractConfigFile.test.json", "./__tests__/test-configs");
expect(config).toEqual({
tests: [
{
id: 1,
name: "Test Extract Config File",
},
],
});
});

it("extractConfigFile works for invalid config file", async () => {
const config = await extractConfigFile("invalidConfigFile.test.json", "./__tests__/test-configs");
expect(config).toEqual({});
});

it("extractConfigFile works for non-existent config file", async () => {
const config = await extractConfigFile("nonExistentConfigFile.test.json", "./__tests__/test-configs");
expect(config).toEqual({});
});

it("extractConfigFile works for non-existent config directory", async () => {
const config = await extractConfigFile("nonExistentConfigFile.test.json", "./__tests__/non-existent-dir");
expect(config).toEqual({});
});
});
41 changes: 41 additions & 0 deletions app/front-end/__tests__/dummy-authenticator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Created on Sun Oct 29 2023
* Author: Connor Doman
*/

import { extractConfigFile } from "@lib/config";
import { DummyAuthenticator } from "@lib/dummy-authenticator";

describe("Dummy Authenticator", () => {
it("works for valid credentials", async () => {
const dummyAuthenticator = new DummyAuthenticator();
const credentials = { email: "johnny@example.com", password: "password" };
DummyAuthenticator.configDirectory = "./conf/";
const user = await dummyAuthenticator.authorize(credentials, {} as any);
expect(user).toEqual({
id: "1",
email: "johnny@example.com",
});
});

it("fails for invalid credentials", async () => {
const dummyAuthenticator = new DummyAuthenticator();
const credentials = { email: "johnny@example.com", password: "wrongpassword" };
DummyAuthenticator.configDirectory = "./conf/";
const user = await dummyAuthenticator.authorize(credentials, {} as any);
expect(user).toEqual(null);
});

it("contains 'credentials' profile name", () => {
const dummyAuthenticator = new DummyAuthenticator();
expect(dummyAuthenticator.name).toEqual("credentials");
});

it("contains correct credentials profile", () => {
const dummyAuthenticator = new DummyAuthenticator();
expect(dummyAuthenticator.credentials).toEqual({
email: { label: "Email", type: "text", placeholder: "Email" },
password: { label: "Password", type: "password" },
});
});
});
4 changes: 2 additions & 2 deletions app/front-end/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import "@testing-library/jest-dom";
/* Example Test: Does the page at `/` render a header? (Should FAIL) */
describe("Home", () => {
it("page has 'PrivacyPal' semantic heading", () => {
render(<Home />);
render(<Home useAuth={false} />);
expect(screen.getByRole("heading", { name: "PrivacyPal" })).toBeInTheDocument();
});

it("homepage is unchanged from snapshot", () => {
const { container } = render(<Home />);
const { container } = render(<Home useAuth={false} />);
expect(container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"tests": [
{
"id": 1,
"name": "Test Extract Config File"
}
]
}
25 changes: 25 additions & 0 deletions app/front-end/conf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# PrivacyPal Configuration

## Users

User data for basic authentication is stored in `./user.properties.json` in the form of:

```typescript
interface PrivacyPalUser {
id: string;
email: string;
password: string;
}

interface PrivacyPalUserProperties {
users: PrivacyPalUser[];
}
```

Where `password` is a base64 encoded `bcrypt` hash of the user's password:

```typescript
const password = btoa(await bcrypt.hash(userPassword, 10));
```

The sample file features a user with the email `johnny@example.com` and password `password`.
9 changes: 9 additions & 0 deletions app/front-end/conf/user.properties.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"users": [
{
"id": "1",
"email": "johnny@example.com",
"hashedPassword": "JDJiJDEwJDQwR0psSWo3Q08zcEhTVkpTVTNhbU9QQmZlYWl1dk1SL2dTMlZCclAuOGIuMjdiZFJTYjVp"
}
]
}
Loading

0 comments on commit f693330

Please sign in to comment.