Skip to content

Commit

Permalink
Implemented Users in Backend
Browse files Browse the repository at this point in the history
  • Loading branch information
R1c4rdCo5t4 committed Jun 11, 2024
1 parent 2c3a598 commit 5d872dc
Show file tree
Hide file tree
Showing 24 changed files with 350 additions and 48 deletions.
2 changes: 1 addition & 1 deletion code/client/src/domain/editor/fugue/Fugue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class Fugue {
*/
deleteLocal(selection: Selection): DeleteOperation[] {
const nodes = Array.from(this.traverseBySelection(selection));
const cursor = {...selection.start}
const cursor = { ...selection.start };
return nodes.map(node => {
if (node.value === '\n') {
cursor.line++;
Expand Down
14 changes: 7 additions & 7 deletions code/client/src/domain/editor/slate/operations/input.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Editor } from 'slate';
import { ReactEditor } from 'slate-react';
import CustomEditor from '@domain/editor/slate/CustomEditor';
import {isEqual, min} from 'lodash';
import { isEqual, min } from 'lodash';
import { getKeyFromInputEvent } from '@domain/editor/slate/utils/domEvents';
import { getSelection, isSelected } from '@domain/editor/slate/utils/selection';
import { Cursor, emptyCursor } from '@domain/editor/cursor';
Expand All @@ -20,13 +20,13 @@ export default (editor: Editor, connector: InputConnector, onFormat: (mark: Inli
if (!key) return;

const selection = getSelection(editor);
console.log("Selection: ", selection)
const cursor = min([{...selection.start}, {...selection.end}]) as Cursor; // always use the start of the selection
console.log("Initial Cursor: ", cursor);
console.log('Selection: ', selection);
const cursor = min([{ ...selection.start }, { ...selection.end }]) as Cursor; // always use the start of the selection
console.log('Initial Cursor: ', cursor);
// if there is a selection, delete the selected text
if (isSelected(editor)) connector.deleteSelection(selection);
console.log("Selection after deletion: ", getSelection(editor));
console.log("Cursor after deletion: ", cursor);
console.log('Selection after deletion: ', getSelection(editor));
console.log('Cursor after deletion: ', cursor);
switch (key) {
case 'Enter':
onEnter(cursor);
Expand Down Expand Up @@ -81,7 +81,7 @@ export default (editor: Editor, connector: InputConnector, onFormat: (mark: Inli
*/
function onKey(key: string, cursor: Cursor) {
const styles = CustomEditor.getMarks(editor) as InlineStyle[];
console.log("Cursor: ", cursor)
console.log('Cursor: ', cursor);
connector.insertCharacter(key, cursor, styles);
}

Expand Down
34 changes: 34 additions & 0 deletions code/client/src/services/auth/authService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { UserData } from '@notespace/shared/src/users/types';
import { HttpCommunication } from '@services/communication/http/httpCommunication';

function authService(http: HttpCommunication) {
async function registerUser(id: string, data: UserData) {
await http.post('/users', { id, ...data });
}

async function registerUserOAuth(id: string, data: UserData) {
await http.post('/users?oauth=true', { id, ...data });
}

async function getUser(id: string) {
return await http.get(`/users/${id}`);
}

async function updateUser(id: string, newProps: Partial<UserData>) {
await http.put(`/users/${id}`, { id, ...newProps });
}

async function deleteUser(id: string) {
await http.delete(`/users/${id}`);
}

return {
registerUser,
registerUserOAuth,
getUser,
updateUser,
deleteUser,
};
}

export default authService;
10 changes: 10 additions & 0 deletions code/client/src/services/auth/useAuthService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useMemo } from 'react';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import authService from '@services/auth/authService';

function useAuthService() {
const { http } = useCommunication();
return useMemo(() => authService(http), [http]);
}

export default useAuthService;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface HttpCommunication {
post: (url: string, data?: any) => Promise<any>;
get: (url: string) => Promise<any>;
put: (url: string, data?: any) => Promise<any>;
delete: (url: string) => Promise<any>;
delete: (url: string, data?: any) => Promise<any>;
}

async function get(url: string) {
Expand All @@ -19,8 +19,8 @@ async function put(url: string, body: any) {
return request(url, 'PUT', body);
}

async function _delete(url: string) {
return request(url, 'DELETE');
async function del(url: string, body: any) {
return request(url, 'DELETE', body);
}

const request = async (url: string, method: string, body?: any) => {
Expand All @@ -43,5 +43,5 @@ export const httpCommunication: HttpCommunication = {
post,
get,
put,
delete: _delete,
delete: del,
};
11 changes: 9 additions & 2 deletions code/client/src/ui/components/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import './Header.scss';
import { useAuth } from '@ui/contexts/auth/useAuth';
import { Link } from 'react-router-dom';

function Header() {
const { currentUser, logout } = useAuth();
return (
<header className="header">
<p></p>
<div>
<p>{currentUser?.email}</p>
<button onClick={logout}>Logout</button>
{currentUser ? (
<>
<p>{currentUser?.email}</p>
<button onClick={logout}>Logout</button>
</>
) : (
<Link to="/login">Login</Link>
)}
</div>
</header>
);
Expand Down
81 changes: 52 additions & 29 deletions code/client/src/ui/contexts/auth/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
import { auth, googleAuthProvider, githubAuthProvider } from '@config';
import { auth, githubAuthProvider, googleAuthProvider } from '@config';
import { createContext, ReactNode, useEffect, useState } from 'react';
import {
signInWithPopup,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signInWithPopup,
signOut,
updateProfile,
User,
UserCredential,
} from 'firebase/auth';
import { validateEmail, validatePassword, validateUsername } from '@ui/contexts/auth/utils';
import useError from '@ui/contexts/error/useError';
import useAuthService from '@services/auth/useAuthService';

export type AuthContextType = {
currentUser: User | null;
login: (email: string, password: string) => Promise<UserCredential>;
loginWithGoogle: () => Promise<UserCredential>;
loginWithGithub: () => Promise<UserCredential>;
signup: (email: string, password: string) => Promise<UserCredential>;
login: (email: string, password: string) => Promise<void>;
loginWithGoogle: () => Promise<void>;
loginWithGithub: () => Promise<void>;
signup: (username: string, email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
updateUserProfile: (user: User, profile: UserProfile) => Promise<void>;
};

export const AuthContext = createContext<AuthContextType>({
currentUser: null,
login: async () => ({}) as UserCredential,
loginWithGoogle: async () => ({}) as UserCredential,
loginWithGithub: async () => ({}) as UserCredential,
signup: async () => ({}) as UserCredential,
login: async () => {},
loginWithGoogle: async () => {},
loginWithGithub: async () => {},
signup: async () => {},
logout: async () => {},
updateUserProfile: async () => {},
});
Expand All @@ -42,30 +44,51 @@ type UserProfile = {
export function AuthProvider({ children }: AuthProviderProps) {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const { publishError } = useError();
const { registerUser, registerUserOAuth } = useAuthService();

function signup(email: string, password: string) {
return createUserWithEmailAndPassword(auth, email, password);
}
const handleAsyncAction = async (action: () => Promise<any>) => {
try {
return await action();
} catch (e) {
console.log(e);
publishError(e as Error);
}
};

function login(email: string, password: string) {
return signInWithEmailAndPassword(auth, email, password);
}
const signup = (username: string, email: string, password: string) =>
handleAsyncAction(async () => {
validateUsername(username);
validateEmail(email);
validatePassword(password);
const { user } = await createUserWithEmailAndPassword(auth, email, password);
await registerUser(user.uid, { username, email });
});

function logout() {
return signOut(auth);
}
const login = (email: string, password: string) =>
handleAsyncAction(async () => {
return signInWithEmailAndPassword(auth, email, password)
.catch(() => {
throw new Error('Invalid credentials');
})
.then(console.log);
});

function updateUserProfile(user: User, profile: UserProfile) {
return updateProfile(user, profile);
}
const logout = () => handleAsyncAction(() => signOut(auth));

function loginWithGoogle() {
return signInWithPopup(auth, googleAuthProvider);
}
const updateUserProfile = (user: User, profile: UserProfile) => updateProfile(user, profile);

function loginWithGithub() {
return signInWithPopup(auth, githubAuthProvider);
}
const loginWithGoogle = () =>
handleAsyncAction(async () => {
const { user } = await signInWithPopup(auth, googleAuthProvider);
await registerUserOAuth(user.uid, { username: user.displayName!, email: user.email! });
});

const loginWithGithub = () =>
handleAsyncAction(async () => {
const { user } = await signInWithPopup(auth, githubAuthProvider);
await registerUserOAuth(user.uid, { username: user.displayName!, email: user.email! });
});

useEffect(() => {
return auth.onAuthStateChanged(user => {
Expand Down
22 changes: 22 additions & 0 deletions code/client/src/ui/contexts/auth/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function validateUsername(username: string) {
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
if (!usernameRegex.test(username)) {
throw new Error('Invalid username');
}
}

export function validateEmail(email: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email');
}
}

export function validatePassword(password: string) {
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
if (!passwordRegex.test(password)) {
throw new Error(
'Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one digit, and one special character.'
);
}
}
4 changes: 2 additions & 2 deletions code/client/src/ui/pages/auth/signup/Signup.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FormEvent } from 'react';
import CssBaseline from '@mui/material/CssBaseline';
import TextField from '@mui/material/TextField';
import OAuth from '@ui/pages/auth/components/OAuth';
Expand All @@ -7,10 +8,9 @@ import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import { FormEvent, useState } from 'react';
import '../Auth.scss';
import { useAuth } from '@ui/contexts/auth/useAuth';
import useError from '@ui/contexts/error/useError';
import '../Auth.scss';

function Signup() {
const navigate = useNavigate();
Expand Down
1 change: 1 addition & 0 deletions code/server/sql/clean_tables.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
begin;

delete from "user";
delete from resource;
delete from workspace;

Expand Down
6 changes: 6 additions & 0 deletions code/server/sql/create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ begin;
children char(16)[] not null default '{}'::char(16)[] -- array of resource ids
);

create table if not exists "user" (
id char(28) primary key,
username text not null,
email text not null unique
);

commit;
2 changes: 1 addition & 1 deletion code/server/sql/drop_tables.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
begin ;

drop table if exists "user";
drop table if exists resource cascade;
drop table if exists workspace cascade;
drop type if exists resource_type cascade;
Expand Down
55 changes: 55 additions & 0 deletions code/server/src/controllers/http/handlers/usersHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import PromiseRouter from 'express-promise-router';
import { Request, Response } from 'express';
import { UsersService } from '@services/UsersService';
import { httpResponse } from '@controllers/http/utils/httpResponse';
import { UserData } from '@notespace/shared/src/users/types';

function usersHandlers(service: UsersService) {
const registerUser = async (req: Request, res: Response) => {
const { id, ...data } = req.body;
const oauth = req.query.oauth === 'true';
if (oauth) {
try {
const user = await service.getUser(id);
if (user) {
// user already registered
httpResponse.noContent(res).send();
return;
}
} catch (e) {
// user not found, continue
}
}
await service.createUser(id, data);
httpResponse.created(res).send();
};

const getUser = async (req: Request, res: Response) => {
const { id } = req.params;
const user = await service.getUser(id);
httpResponse.ok(res).json(user);
};

const updateUser = async (req: Request, res: Response) => {
const { id } = req.params;
const { ...data } = req.body as UserData;
await service.updateUser(id, data);
httpResponse.noContent(res).send();
};

const deleteUser = async (req: Request, res: Response) => {
const { id } = req.params;
await service.deleteUser(id);
httpResponse.noContent(res).send();
};

const router = PromiseRouter({ mergeParams: true });
router.post('/', registerUser);
router.get('/:id', getUser);
router.put('/:id', updateUser);
router.delete('/:id', deleteUser);

return router;
}

export default usersHandlers;
2 changes: 2 additions & 0 deletions code/server/src/controllers/http/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PromiseRouter from 'express-promise-router';
import { Services } from '@services/Services';
import workspacesHandlers from '@controllers/http/handlers/workspacesHandlers';
import errorHandler from '@controllers/http/handlers/errorHandler';
import usersHandlers from '@controllers/http/handlers/usersHandlers';
import { Server } from 'socket.io';

export default function (services: Services, io: Server) {
Expand All @@ -12,6 +13,7 @@ export default function (services: Services, io: Server) {
router.use(express.urlencoded({ extended: true }));

router.use('/workspaces', workspacesHandlers(services, io));
router.use('/users', usersHandlers(services.users));
router.use(errorHandler);
return router;
}
Loading

0 comments on commit 5d872dc

Please sign in to comment.