From 8efbedabaad6538c6e4f2546fa490782132f7b83 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:08:15 -0500 Subject: [PATCH 01/14] add cors to backend --- backend/internal/server/index.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/internal/server/index.go b/backend/internal/server/index.go index b9f1457f..3ee8040d 100644 --- a/backend/internal/server/index.go +++ b/backend/internal/server/index.go @@ -10,6 +10,7 @@ import ( "github.com/labstack/echo/v4" echoMiddleware "github.com/labstack/echo/v4/middleware" + "KonferCA/SPUR/common" "KonferCA/SPUR/db" "KonferCA/SPUR/internal/middleware" ) @@ -69,6 +70,19 @@ func New(testing bool) (*Server, error) { // setup error handler and middlewares e.HTTPErrorHandler = globalErrorHandler + if os.Getenv("APP_ENV") == common.DEVELOPMENT_ENV { + // use default cors config, allow everything in development + e.Use(echoMiddleware.CORS()) + } else if os.Getenv("APP_ENV") == common.PRODUCTION_ENV { + e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{ + AllowOrigins: []string{"https://spur.konfer.ca"}, + })) + } else { + e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{ + AllowOrigins: []string{"https://nk-preview.konfer.ca"}, + })) + } + e.Use(middleware.Logger()) e.Use(echoMiddleware.Recover()) e.Use(apiLimiter.RateLimit()) // global rate limit From c1ae7a46c75c5a61f7fa95bece8dda8a40101598 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:08:34 -0500 Subject: [PATCH 02/14] add api utils --- frontend/src/utils/api.ts | 24 ++++++++++++++++++++++++ frontend/src/utils/index.ts | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 frontend/src/utils/api.ts diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts new file mode 100644 index 00000000..64febdec --- /dev/null +++ b/frontend/src/utils/api.ts @@ -0,0 +1,24 @@ +/* + * Http status code enum for easy access and type safety. + */ +export enum HttpStatusCode { + OK = 200, + CREATED = 201, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + INTERNAL_SERVER_ERROR = 500, +} + +/* + * Constructs a url string with the right backend url and given path. + * The path can be optional. + * The url returned is for APIs. + */ +export function getApiUrl(path: string = '') { + // remove leading / if any + if (path.length > 0 && path[0] === '/') path = path.slice(1); + const prefix = import.meta.env.VITE_API_URL; + return `${prefix}/${path}`; +} diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index b68c8a53..23ef9ed8 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -6,3 +6,5 @@ export { scrollToTop, isElementInView, } from './scroll'; + +export { getApiUrl, HttpStatusCode } from './api'; From 6b955caedfe7c432625ec6f2bebc4e82f007a3c2 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:08:49 -0500 Subject: [PATCH 03/14] update some frontend configuration --- frontend/src/vite-env.d.ts | 8 ++++++++ frontend/tsconfig.app.json | 3 ++- frontend/vite.config.ts | 6 +++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 11f02fe2..132786a3 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1 +1,9 @@ /// + +interface ImportMetaEnv { + readonly VITE_API_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index ba4a646a..4fe0800d 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -15,7 +15,8 @@ "@pages": ["./src/pages/index"], "@utils": ["./src/utils/index"], "@assets": ["./src/assets/index"], - "@t": ["./src/types/index"] + "@t": ["./src/types/index"], + "@services": ["./src/services/index"] }, /* Bundler mode */ diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7468d0e7..a30fc2cf 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -9,10 +9,14 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), '@assets': path.resolve(__dirname, './src/assets'), '@components': path.resolve(__dirname, './src/components'), - '@components/layout': path.resolve(__dirname, './src/components/layout'), + '@components/layout': path.resolve( + __dirname, + './src/components/layout' + ), '@pages': path.resolve(__dirname, './src/pages'), '@utils': path.resolve(__dirname, './src/utils'), '@t': path.resolve(__dirname, './src/types'), + '@services': path.resolve(__dirname, './src/services'), }, }, }); From 970a1305787ababd9322c29e61ad743a1bd753ec Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:08:54 -0500 Subject: [PATCH 04/14] add new user types --- frontend/src/types/auth.ts | 11 +++++++++++ frontend/src/types/index.ts | 1 + 2 files changed, 12 insertions(+) create mode 100644 frontend/src/types/auth.ts diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts new file mode 100644 index 00000000..7deb5863 --- /dev/null +++ b/frontend/src/types/auth.ts @@ -0,0 +1,11 @@ +export type UserRole = 'startup_owner' | 'admin' | 'investor'; + +export interface User { + id: string; + email: string; + firstName: string; + lastName: string; + role: string; + walletAddress: string; + isEmailVerified: boolean; +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 49559522..4bad588f 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,2 +1,3 @@ export * from './common'; export * from './form'; +export type { User, UserRole } from './auth'; From b91b37213c316b77677883bd0f3e49076fb70da4 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:09:20 -0500 Subject: [PATCH 05/14] add auth service register --- frontend/src/services/auth.ts | 54 +++++++++++++++++++++++++++++++++ frontend/src/services/errors.ts | 22 ++++++++++++++ frontend/src/services/index.ts | 2 ++ 3 files changed, 78 insertions(+) create mode 100644 frontend/src/services/auth.ts create mode 100644 frontend/src/services/errors.ts create mode 100644 frontend/src/services/index.ts diff --git a/frontend/src/services/auth.ts b/frontend/src/services/auth.ts new file mode 100644 index 00000000..3b0cf3b1 --- /dev/null +++ b/frontend/src/services/auth.ts @@ -0,0 +1,54 @@ +/** + * This file contains business logic regarding authentication. + */ + +import { getApiUrl, HttpStatusCode } from '@utils'; +import { RegisterError } from './errors'; + +import type { User, UserRole } from '@t'; + +export interface RegisterReponse { + accessToken: string; + refreshToken: string; + user: User; +} + +/** + * Registers a user if the given email is not already registered. + */ +export async function register( + email: string, + password: string, + role: UserRole = 'startup_owner' +): Promise { + const url = getApiUrl('/auth/signup'); + const body = { + email, + password, + role, + }; + + const res = await fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + }); + // the backend should always return json for the api calls + const json = await res.json(); + + if (res.status !== HttpStatusCode.CREATED) { + throw new RegisterError('Failed to register', res.status, json); + } + + return json as RegisterReponse; +} + +/** + * Saves the refresh token in localStorage. + */ +export function saveRefreshToken(refreshToken: string) { + // IMPORTANT: The location on where the token is saved must be revisited. + localStorage.setItem('refresh_token', refreshToken); +} diff --git a/frontend/src/services/errors.ts b/frontend/src/services/errors.ts new file mode 100644 index 00000000..bc1b4388 --- /dev/null +++ b/frontend/src/services/errors.ts @@ -0,0 +1,22 @@ +export const API_ERROR = 'ApiError'; +export const REGISTER_ERROR = 'RegisterError'; + +export class ApiError extends Error { + // statusCode represents the status of the response + public statusCode: number; + public body: any; + + constructor(message: string, statusCode: number, body: any) { + super(message); + this.name = API_ERROR; + this.statusCode = statusCode; + this.body = body; + } +} + +export class RegisterError extends ApiError { + constructor(message: string, statusCode: number, body: any) { + super(message, statusCode, body); + this.name = REGISTER_ERROR; + } +} diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts new file mode 100644 index 00000000..86eb35d9 --- /dev/null +++ b/frontend/src/services/index.ts @@ -0,0 +1,2 @@ +export { register, saveRefreshToken } from './auth'; +export { RegisterError, ApiError, API_ERROR, REGISTER_ERROR } from './errors'; From 25ba0ff87f47c93a512fdae8f4cfa38d35f4769e Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:09:31 -0500 Subject: [PATCH 06/14] handle register form submission --- frontend/src/pages/Register.tsx | 147 +++++++++++++++++++------------- 1 file changed, 89 insertions(+), 58 deletions(-) diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index 096e5bef..e71edf0f 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -1,7 +1,8 @@ import { useState, useEffect, FormEvent } from 'react'; import { Button, TextInput, TextArea } from '@components'; +import { register, RegisterError, saveRefreshToken } from '@services'; -type RegistrationStep = +type RegistrationStep = | 'login-register' | 'verify-email' | 'signing-in' @@ -25,7 +26,8 @@ interface FormErrors { } const Register = () => { - const [currentStep, setCurrentStep] = useState('login-register'); + const [currentStep, setCurrentStep] = + useState('login-register'); const [formData, setFormData] = useState({ firstName: '', lastName: '', @@ -33,11 +35,12 @@ const Register = () => { bio: '', linkedIn: '', email: '', - password: '' + password: '', }); const [errors, setErrors] = useState({}); - const LINKEDIN_REGEX = /^(https?:\/\/)?([\w]+\.)?linkedin\.com\/(pub|in|profile)\/([-a-zA-Z0-9]+)\/?$/; + const LINKEDIN_REGEX = + /^(https?:\/\/)?([\w]+\.)?linkedin\.com\/(pub|in|profile)\/([-a-zA-Z0-9]+)\/?$/; const validateLinkedIn = (url: string): boolean => { if (!url) return false; @@ -46,42 +49,64 @@ const Register = () => { const handleLinkedInChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - [name]: value + [name]: value, })); if (value && !validateLinkedIn(value)) { - setErrors(prev => ({ + setErrors((prev) => ({ ...prev, - linkedIn: "Please enter a valid LinkedIn profile URL" + linkedIn: 'Please enter a valid LinkedIn profile URL', })); } else { - setErrors(prev => ({ + setErrors((prev) => ({ ...prev, - linkedIn: undefined + linkedIn: undefined, })); } }; - const handleChange = (e: React.ChangeEvent) => { + const handleChange = ( + e: React.ChangeEvent + ) => { const { name, value } = e.target; - setFormData(prev => ({ + setFormData((prev) => ({ ...prev, - [name]: value + [name]: value, })); if (errors[name as keyof FormErrors]) { - setErrors(prev => ({ + setErrors((prev) => ({ ...prev, - [name]: undefined + [name]: undefined, })); } }; - const handleInitialSubmit = (e: FormEvent) => { + const handleInitialSubmit = async (e: FormEvent) => { e.preventDefault(); - setCurrentStep('verify-email'); + try { + // user information and access token should be stored in an auth provider + const regResp = await register(formData.email, formData.password); + console.log(regResp); + + // save the refresh token in local storage and use it to request a new + // access token later when the user reopens the web app. + // IMPORTANT: for now localstorage works, but by MVP we need a more proper + // way to handle authentication using HTTP-only cookies and adding + // fingerprint information to tokens to prevet XSS. + // for more info: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html#token-storage-on-client-side + saveRefreshToken(regResp.refreshToken); + setCurrentStep('verify-email'); + } catch (error) { + if (error instanceof RegisterError) { + // TODO: handle error with some kind of notification + console.log('do something here', error.statusCode, error.body); + } else { + // TODO: handle error with some kind of notification + } + } }; const handleFormSubmit = (e: FormEvent) => { @@ -95,7 +120,7 @@ const Register = () => { formData.lastName.trim() !== '' && formData.position.trim() !== '' && formData.bio.trim() !== '' && - formData.linkedIn.trim() !== '' && + formData.linkedIn.trim() !== '' && !errors.linkedIn ); }; @@ -104,41 +129,35 @@ const Register = () => {

Register or Login


-

Register for Spur+Konfer

- +

+ Register for Spur+Konfer +

+
- - - - -
-

- Already have an account? -

+

Already have an account?

+
); @@ -188,29 +212,30 @@ const Register = () => {

Welcome to Spur+Konfer

- To begin your application, please enter your organization's details + To begin your application, please enter your organization's + details

- - - { />