Skip to content
This repository has been archived by the owner on May 24, 2022. It is now read-only.

Commit

Permalink
Merge pull request #135 from SELab-2/development
Browse files Browse the repository at this point in the history
merge frontend code with production
  • Loading branch information
TomAlard authored Mar 24, 2022
2 parents 3906868 + 9128831 commit 07f1792
Show file tree
Hide file tree
Showing 25 changed files with 950 additions and 284 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Add env file to frontend directory
run: ln /home/selab2/.env .
working-directory: ./frontend
- name: Install and update yarn
run: npm install -g yarn
- name: Install yarn dependencies
Expand Down
1 change: 0 additions & 1 deletion backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class SecurityConfig(val userDetailsService: OsocUserDetailService) : WebSecurit
override fun configure(http: HttpSecurity) {
http.csrf().disable()

// THIS IS A TEMPORARY FIX SOMEBODY SHOULD LOOK UP HOW CORS SHOULD BE ENABLED CORRECTLY
http.cors().disable()

http.sessionManagement().maximumSessions(1)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

Expand Down
6 changes: 5 additions & 1 deletion frontend/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ frontend/*
!frontend/public/
!frontend/src/
!frontend/styles/
!frontend/lib/
!frontend/atoms/

# Ignores for manual runs
/*
!pages/
!public/
!src/
!styles/
!styles/
!lib/
!atoms/
5 changes: 2 additions & 3 deletions frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,16 @@ module.exports = {
plugins: [
'@typescript-eslint',
'react',
'eslint-plugin-tsdoc'
'eslint-plugin-tsdoc',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'prettier'
],
rules: {
'tsdoc/syntax': 'warn'
'tsdoc/syntax': 'warn',
}
};
6 changes: 5 additions & 1 deletion frontend/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ frontend/*
!frontend/public/
!frontend/src/
!frontend/styles/
!frontend/lib/
!frontend/atoms/

# Ignores for manual runs
/*
!pages/
!public/
!src/
!styles/
!styles/
!lib/
!atoms/
10 changes: 10 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ Run the built app in production mode.
yarn start
```

## Environment Variables

For development, you should put these variables in a `.env.local` file.
For production a normal `.env` suffices, but this file needs to be instantiated on build time, otherwise it won't get loaded succesfully.
The `NEXT_PUBLIC` prefix exposes the env var to the whole application, not just the NextJS specific items.

```sh
NEXT_PUBLIC_API_ENDPOINT=... # URL of the backend, default should be http://localhost:8080/api
```

## Running linters
While in /frontend run following commands to show style errors
```shell
Expand Down
32 changes: 32 additions & 0 deletions frontend/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* IMPORTANT NOTE
*
* A certain feature of NextJS (at the time of writing, it is unclear which feature this is) causes
* Recoil to throw errors because of duplicate atom keys. This of course isn't the case and it doesn't break when used.
* To get around this issue, we are generating a random uuid every time this file is re-run, that way we won't get the
* duplicate key warning. This seems like an unsafe way to fix this, but I can assure you, it works as intended.
*/
import { atom, GetCallback, GetRecoilValue, selector } from 'recoil';
import { v4 } from 'uuid';

type UniqueAtomParams<T> = {
name: string;
defaultValue: T;
};

export const uniqueAtom = <T>({ name, defaultValue }: UniqueAtomParams<T>) =>
atom({
key: `${name}/${v4()}`,
default: defaultValue,
});

type UniqueSelectorParams = {
name: string;
getter: (opts: { get: GetRecoilValue; getCallback: GetCallback }) => unknown;
};

export const uniqueSelector = ({ name, getter }: UniqueSelectorParams) =>
selector({
key: `${name}/${v4()}`,
get: getter,
});
65 changes: 65 additions & 0 deletions frontend/atoms/registerAtoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { uniqueAtom, uniqueSelector } from '.';
import { customPasswordRegex, emailRegex, nameRegex } from '../lib/regex';

export const nameState = uniqueAtom({
name: 'registerNameState',
defaultValue: '',
});

export const validNameState = uniqueSelector({
name: 'validNameState',
getter: ({ get }) => {
const name = get(nameState);

// we use a name Regex to check if the name is valid
return nameRegex.test(name);
},
});

export const emailState = uniqueAtom({
name: 'registerEmailState',
defaultValue: '',
});

export const validEmailState = uniqueSelector({
name: 'validEmailState',
getter: ({ get }) => {
const email = get(emailState);

// we use an email Regex to check if the email is valid
return emailRegex.test(email);
},
});

export const passwordState = uniqueAtom({
name: 'registerPasswordState',
defaultValue: '',
});

export const validPasswordState = uniqueSelector({
name: 'validPasswordState',
getter: ({ get }) => {
const password = get(passwordState);

// we use a password Regex to check if we are dealing with a valid password
return customPasswordRegex.test(password);
},
});

export const repeatPasswordState = uniqueAtom({
name: 'registerRepeatPasswordState',
defaultValue: '',
});

export const validRepeatPasswordState = uniqueSelector({
name: 'validRepeatPasswordState',
getter: ({ get }) => {
const password = get(passwordState);
const repeatPassword = get(repeatPasswordState);

// check if it's a valid password and the same as the first password
return (
customPasswordRegex.test(repeatPassword) && password === repeatPassword
);
},
});
83 changes: 83 additions & 0 deletions frontend/components/FormContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { FC, PropsWithChildren } from 'react';

/**
* Parameters for FormContainer Component
* {@label FormContainerProps}
*
* @see PropsWithChildren
*/
type FormContainerProps = PropsWithChildren<{
/**
* Title of the page that is being displayed
*/
pageTitle: string;
}>;

/**
* Container for register and login forms
*
* @remarks
* This is a container with 2 colums with 2 images on either side of a central form,
* which is passed as a child component.
*
* {@label FormContainer}
*
* @see {@link FormContainerProps}
*/
const FormContainer: FC<FormContainerProps> = ({ pageTitle, children }) => {
return (
<>
<div className="h-screen bg-[url('../public/img/login.png')] bg-center">
{/* Left side images */}
<div
className="lg:rounded-5xl relative top-1/2 m-auto flex w-11/12 max-w-md -translate-y-1/2 flex-col items-center
rounded-md bg-[#F3F3f3] px-4 py-4 text-center
md:w-11/12 lg:grid lg:w-10/12 lg:max-w-7xl lg:grid-cols-2 lg:gap-2 xl:grid-cols-3"
>
<div className="hidden max-w-lg place-self-center xl:block">
<img
src="https://osoc.be/img/pictures/osoc17-1.jpg"
alt="image of 4 people posing in front of a wall with post-its"
className="object-scale-down shadow-sm shadow-gray-600 xl:mb-4"
/>
<img
src="https://i0.wp.com/blog.okfn.org/files/2018/08/image3.jpg?fit=1200%2C800&ssl=1"
alt="Group of people cheering on OSOC"
className="object-scale-down shadow-sm shadow-gray-600"
/>
</div>
{/* Main form component */}
<div className="flex max-h-full max-w-full flex-col items-center justify-center">
<header className="flex flex-row items-center justify-center gap-4 pb-5 lg:align-top">
<h1 className="float-left text-3xl font-bold text-osoc-blue sm:text-4xl">
{pageTitle}
</h1>
<img
src="https://osoc.be/img/logo/logo-osoc-color.svg"
className="hidden h-16 w-16 sm:inline-block md:h-24 md:w-24 lg:h-32 lg:w-32"
alt="The OSOC logo"
/>
</header>

{children}
</div>
{/* Right side images */}
<div className="hidden max-w-lg place-self-center lg:block">
<img
src="https://osoc.be/img/pictures/osoc17-2.jpg"
alt="image of 4 people standing around a wall with post-its"
className="object-scale-down shadow-sm shadow-gray-600 lg:mb-4"
/>
<img
src="https://osoc.be/img/pictures/osoc17-3.jpg"
alt="image of someone trying to give you a fistbump"
className="object-scale-down shadow-sm shadow-gray-600"
/>
</div>
</div>
</div>
</>
);
};

export default FormContainer;
13 changes: 13 additions & 0 deletions frontend/lib/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const envURL: string = process.env.NEXT_PUBLIC_API_ENDPOINT || '';
const baseURL = envURL.replace(/\/$/, '');

/**
* Typescript doesn't allow computed enums, this is a little work-around to get the required functionality
*/
type Endpoints = typeof Endpoints[keyof typeof Endpoints];
const Endpoints = {
USERS: baseURL + '/users',
LOGIN: baseURL + '/login',
} as const;

export default Endpoints;
10 changes: 10 additions & 0 deletions frontend/lib/regex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// source: https://stackoverflow.com/questions/2385701/regular-expression-for-first-and-last-name
export const nameRegex =
/^[a-zA-ZàáâäãåąčćęèéêëėįìíîïłńòóôöõøùúûüųūÿýżźñçčšžÀÁÂÄÃÅĄĆČĖĘÈÉÊËÌÍÎÏĮŁŃÒÓÔÖÕØÙÚÛÜŲŪŸÝŻŹÑßÇŒÆČŠŽð ,.'-]+$/u;
// source: https://stackoverflow.com/questions/201323/how-can-i-validate-an-email-address-using-a-regular-expression
/* eslint-disable */
export const emailRegex =
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;

// A password can exist out of any characters, as long as it is between length 8 and 64
export const customPasswordRegex = /^.{8,64}$/;
8 changes: 7 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@
"prettier:write": "yarn prettier --write ."
},
"dependencies": {
"@heroicons/react": "^1.0.6",
"next": "latest",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-hot-toast": "^2.2.0",
"react-preloader-icon": "^1.0.0",
"recoil": "^0.6.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/node": "17.0.4",
"@types/react": "17.0.38",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"autoprefixer": "^10.4.0",
Expand Down
17 changes: 14 additions & 3 deletions frontend/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import '../styles/globals.css';
import '../styles/line.css';
import type { AppProps } from 'next/app';
import { RecoilRoot } from 'recoil';
import { Toaster } from 'react-hot-toast';

function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
function App({ Component, pageProps: { pageProps } }: AppProps) {
return (
<>
{/* RecoilRoot exposes the whole application to the Recoil state manager */}
<RecoilRoot>
<Component {...pageProps} />
<Toaster position="top-right" />
</RecoilRoot>
</>
);
}

export default MyApp;
export default App;
39 changes: 39 additions & 0 deletions frontend/pages/_middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextApiRequest } from 'next';
import { NextRequest, NextResponse } from 'next/server';

/*
* A 'hacky' way to get around the next typing issues for middelware
*/
type NextMiddlewareRequest = NextApiRequest & NextRequest;

/**
* Middleware for every Next request
*
* @param req - the incoming Next request
* @returns a NextResponse object containing the next step
*/
export async function middleware(req: NextMiddlewareRequest) {
// Token will exist if user is logged in
const token = '';
console.log(req);

const { pathname } = req.nextUrl;

/**
* We allow the request to be made if:
* - It contains a valid token (which means the user is logged in)
*/

// clone the url to use in the redirect because NextJS 12.1 does not allow relative URLs anymore
const url = req.nextUrl.clone();
url.pathname = '/login';

// if the request is trying to access protected resources, we redirect them to the login
// TODO: remove /wait page filter when accounts are working
if (
!token &&
!(pathname === '/login' || pathname === '/register' || pathname === '/wait')
) {
return NextResponse.redirect(url);
}
}
Loading

0 comments on commit 07f1792

Please sign in to comment.