Skip to content
Snippets Groups Projects
Commit ab6091f0 authored by D'Abrantes, Felipe (UG - Comp Sci & Elec Eng)'s avatar D'Abrantes, Felipe (UG - Comp Sci & Elec Eng)
Browse files

Merge branch 'friend-service' into 'main'

Add Friend service

See merge request !15
parents d464d137 cd804734
No related branches found
No related tags found
1 merge request!15Add Friend service
Showing
with 708 additions and 0 deletions
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
\ No newline at end of file
FROM node:latest as base
# Create app directory
WORKDIR /friend-service/app
COPY package*.json ./
RUN npm install
# Bundle app source
COPY . .
EXPOSE 9000
CMD [ "npm", "run", "start:dev" ]
\ No newline at end of file
export default {
JWT_SECRET: ""
}
import express, { Application } from 'express'
import { initializeRoutes } from './src/Startup/routes'
import { MongoConnectionProvider } from './src/Database/MongoConnectionProvider'
import jwt from 'jsonwebtoken'
// Server configuration
const server:Application = express();
// InitaliseRoutes
initializeRoutes(server)
// Database configuration
export const MongoClient = new MongoConnectionProvider(process.env.MONGO_DBNAME as string, process.env.MONGO_HOST as string, Number(process.env.MONGO_PORT as string))
MongoClient.Connect()
// MongoClient.Connection.dropDatabase()
console.log(jwt.sign('a', "abcdefg12345"))
console.log(jwt.sign('b', "abcdefg12345"))
console.log(jwt.sign('c', "abcdefg12345"))
server.listen(9000, ():void => {
console.log('Service: Running here 👉 https://localhost:9000')
})
\ No newline at end of file
{
"name": "friend-service",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "npm run build && node app.ts",
"start:dev": "nodemon"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"typescript": "^5.0.4"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.1",
"nodemon": "^2.0.22",
"ts-node": "^10.9.1"
}
}
import mongoose from "mongoose";
/**
* Useful utilities for connection to a mongo database
*/
export class MongoConnectionProvider {
/**
* Mongo db name
*/
public DatabaseName: string;
/**
* Mongo db path
*/
public DatabasePath: string;
/**
* Mongo db port
*/
public DatabasePort: number;
/**
* Mongo db connection
*/
public Connection: mongoose.Connection = mongoose.connection;
constructor(name: string, path: string, port: number) {
this.DatabaseName = name;
this.DatabasePath = path;
this.DatabasePort = port;
}
/**
* Defines connection event functionality
* @param connectionUri
*/
private initializeConnectionEvents = (connectionUri: string) => {
this.Connection
.on('error', () => console.log(`Error connecting to database on: ${connectionUri}`))
.once('open', () => console.log(`Successful connection to database on: ${connectionUri}`))
}
/**
* Constructs the MongoDb connection URI
* @returns
*/
public ConnectionUri = (): string => `mongodb://${this.DatabasePath}:${this.DatabasePort}/${this.DatabaseName}`
/**
* Initalises a new mongo connection.
*/
public Connect = async (): Promise<void> => {
const connectionUri = this.ConnectionUri()
this.initializeConnectionEvents(connectionUri)
await mongoose.connect(connectionUri).catch((err) => {
console.log(err)
})
}
}
\ No newline at end of file
import mongoose, { Schema } from 'mongoose'
/**
* Schema for representing an a friend
*/
const FriendSchema = new Schema({
User1: {
type: String,
required: true
},
User2: {
type: String,
required: true
}
})
export default FriendSchema;
\ No newline at end of file
import mongoose, { Schema } from 'mongoose'
/**
* Schema for representing an a request
*/
const RequestSchema = new Schema({
SourceUser: {
type: String,
required: true
},
TargetUser: {
type: String,
required: true
}
})
export default RequestSchema;
\ No newline at end of file
import mongoose, { Model, Schema } from 'mongoose'
/**
* Defines common datastore functionality
*/
export abstract class DataStore<T> {
/**
* Mongoose model pertaining to the datastore type
*/
public Model: Model<any>
constructor (modelName: string, schema: Schema) {
this.Model = mongoose.model(modelName, schema)
}
/**
* Finds a single item matching a given query
* @param query
* @returns
*/
public GetItem = async (query: any): Promise<T | null> => {
return await this.Model.findOne(query)
}
}
\ No newline at end of file
import FriendSchema from '../Database/Schemas/FriendSchema';
import RequestSchema from '../Database/Schemas/RequestSchema';
import { Friend } from '../Types/Friend';
import { DataStore } from './DataStore';
/**
* Contains actions pertaining to storing and accessing Friends
*/
class FriendDataStore extends DataStore<any>{
/**
* Create a new friend relation between two users.
* @param u1
* @param u2
* @returns
*/
public newFriend = async (u1: string, u2: string): Promise<Friend> => {
if(await this.Model.findOne({User1: u1, User2: u2}) !== null || await this.Model.findOne({User1: u2, User2: u1}) !== null){
throw new Error(`${u1} and ${u2} are already friends!`);
}
return await this.Model.create({
User1: u1,
User2: u2
});
};
/**
* Store action for getting a users friends
* @param itemCount
* @returns
*/
public getFriends = async (userId: string): Promise<{friends1: Friend[], friends2: Friend[]}> => {
const friends1 = await this.Model.find({User1: userId})
const friends2 = await this.Model.find({User2: userId})
return {friends1, friends2}
}
/**
* Method to removes a friend relation
* @param user1
* @param user2
*/
public RemoveFriend = async (user1: string, user2: string): Promise<void> => {
await this.Model.findOneAndDelete({User1: user1, User2: user2})
await this.Model.findOneAndDelete({User1: user2, User2: user1})
}
/**
* Get the friend item by its id.
* @param id
* @returns
*/
public GetFriendByUsers = async (user1: string, user2: string): Promise<Friend | null> => {
const result = await this.Model.findOne({User1: user1, User2: user2}) || await this.Model.findOne({User1: user2, User2: user1});
return result;
}
}
export default new FriendDataStore('Friend', FriendSchema)
\ No newline at end of file
import RequestSchema from '../Database/Schemas/RequestSchema';
import { FriendRequest } from '../Types/Request';
import { DataStore } from './DataStore';
/**
* Contains actions pertaining to storing and accessing Requests
*/
class RequestDataStore extends DataStore<any>{
public newRequest = async (sourceUserId: string, targetUserId: string): Promise<FriendRequest> => {
return await this.Model.create({
SourceUser: sourceUserId,
TargetUser: targetUserId
});
};
/**
* Store action for getting requests for a user
* @param itemCount
* @returns
*/
public getRequests = async (userId: string): Promise<FriendRequest[]> => {
return await this.Model.find({TargetUser: userId}).sort({$natural: -1})
}
/**
* Find and delete a request if it exists.
* @param requestId
* @returns
*/
public handleRequestById = async (requestId: string): Promise<FriendRequest> => {
const result = await this.Model.findByIdAndDelete(requestId)
if (result === null){
throw new Error ("Invalid request id!")
}
return result;
}
/**
* Get a request by its id
* @param requestId
* @returns
*/
public GetRequestById = async (requestId: string): Promise<FriendRequest> => {
const result = await this.Model.findById(requestId)
if(result === null){
throw new Error ("Invalid request id!")
}
return result;
}
}
export default new RequestDataStore('Request', RequestSchema)
\ No newline at end of file
import FriendDataStore from "../Datastores/FriendDataStore";
import { Friend } from "../Types/Friend";
/**
* Class to handle all friend functionality
*/
export class FriendManager {
public AddFriend = async (user1: string, user2: string): Promise<Friend> => {
return await FriendDataStore.newFriend(user1, user2);
}
public GetFriendIds = async (userId: string): Promise<string[]> => {
const friends = await FriendDataStore.getFriends(userId);
return friends.friends1.map(f => f.User2).concat(friends.friends2.map(f => f.User1));
}
public RemoveFriend = async (user1: string, user2: string): Promise<void> => {
if(await FriendDataStore.GetFriendByUsers(user1, user2) === null){
throw new Error("Users are not friends!");
}
return await FriendDataStore.RemoveFriend(user1, user2);
}
}
\ No newline at end of file
import jwt, { Secret, JwtPayload } from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
import Config from '../../config'
export const SECRET_KEY: Secret = Config.JWT_SECRET;
export interface CustomJWTRequest extends Request {
token?: string | JwtPayload;
}
export const authorize = async (req: Request, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization;
const token = authHeader?.split(" ")[1];
if (!token) {
throw new Error();
}
const decoded = jwt.verify(token, SECRET_KEY);
(req as CustomJWTRequest).token = decoded;
next();
} catch (err) {
console.log(err)
res.status(401).send('Please authenticate');
}
};
\ No newline at end of file
import FriendDataStore from "../Datastores/FriendDataStore";
import RequestDataStore from "../Datastores/RequestDataStore"
import { FriendManager } from "../Friends/FriendManager";
import { Friend } from "../Types/Friend";
import { FriendRequest } from "../Types/Request";
/**
* Handles all friend request functionality
*/
export class RequestManager {
private friendManager: FriendManager
constructor(friendManager: FriendManager){
this.friendManager = friendManager;
}
/**
* Method to create a new friend request
* @param sourceId
* @param targetId
* @returns
*/
public NewRequest = async (sourceId: string, targetId: string):Promise<FriendRequest> => {
if(sourceId === targetId){
throw new Error("Sorry, can't friend yourself! :(");
}
if(await RequestDataStore.GetItem({SourceUser: sourceId, TargetUser: targetId}) !== null || await RequestDataStore.GetItem({SourceUser: targetId, TargetUser: sourceId}) !== null){
throw new Error("Request already exists!");
}
if(await FriendDataStore.GetFriendByUsers(sourceId, targetId) !== null){
throw new Error("Users are already friends!");
}
return await RequestDataStore.newRequest(sourceId, targetId);
}
/**
*
* @param targetId Method to get user requests
* @returns
*/
public GetRequests = async (targetId: string):Promise<FriendRequest[]> => {
return await RequestDataStore.getRequests(targetId)
}
public AcceptRequest = async (requestId: string): Promise<Friend> => {
const request = await RequestDataStore.handleRequestById(requestId)
return await this.friendManager.AddFriend(request.SourceUser, request.TargetUser)
}
public RejectRequest = async (requestId: string): Promise<FriendRequest> => {
return await RequestDataStore.handleRequestById(requestId);
}
public GetSingleRequest = async (requestId: string): Promise<FriendRequest> => {
return await RequestDataStore.GetRequestById(requestId);
}
}
\ No newline at end of file
import express, { Response } from 'express'
import { FriendManager } from '../Friends/FriendManager'
import { CustomJWTRequest } from '../Middleware/Auth'
export const FriendsRouter = express.Router()
const friendManager = new FriendManager();
/**
* GET '/'
* Returns a string
*/
FriendsRouter.get('/', async (req:CustomJWTRequest, res:Response): Promise<void> => {
const {token} = req
return friendManager.GetFriendIds(token as string).then(result => {
res.status(200).json({result: result});
}).catch((error: Error) => {
res.status(400).json({error: error})
});
});
/**
* Delete '/'
* Returns a string
*/
FriendsRouter.delete('/', async (req:CustomJWTRequest, res:Response): Promise<Response> => {
const {user_id, friend_id} = req.body
const {token} = req
if(token !== user_id){
return res.status(400).json({error: 'unauthorised'})
}
return friendManager.RemoveFriend(user_id, friend_id).then(result => {
return res.sendStatus(200);
}).catch((error: Error) => {
return res.status(400).json({error: error.message})
});
});
\ No newline at end of file
import express, { Request, Response } from 'express'
import { FriendManager } from '../Friends/FriendManager';
import { CustomJWTRequest } from '../Middleware/Auth';
import { RequestManager } from '../Requests/RequestManager'
export const RequestRouter = express.Router();
const friendManager = new FriendManager();
const requestManager = new RequestManager(friendManager);
/**
* GET '/'
* Returns a string
*/
RequestRouter.get('/', async (req:CustomJWTRequest, res:Response): Promise<Response> => {
const {token} = req;
return requestManager.GetRequests(token as string).then((result) => {
return res.status(200).json({requests: result})
}).catch((error: Error) => {
return res.status(401).json({error: error.message})
});
});
/**
* POST '/'
* Creates a friend request
*/
RequestRouter.post('/', async (req:CustomJWTRequest, res:Response): Promise<Response> => {
const {requester_id, receiver_id} = req.body;
const {token} = req
if(requester_id !== token){
return res.status(401).json({error: "unathorised"})
}
return requestManager.NewRequest(requester_id, receiver_id).then((result) => {
return res.sendStatus(200)
}).catch((error:Error) => {
return res.status(400).json({error: error.message})
});
});
/**
* PUT '/accept'
*/
RequestRouter.put('/accept', async (req:CustomJWTRequest, res:Response): Promise<Response> => {
const {request_id} = req.body;
const {token} = req;
return await requestManager.GetSingleRequest(request_id).then(result => {
if(result.TargetUser !== token){
throw new Error("Unauthorised")
} else {
return requestManager.AcceptRequest(request_id).then((result) => {
return res.sendStatus(200)
}).catch((error: Error) => {
return res.status(400).json({error: error.message})
})
}
}).catch((error: Error) => {
return res.status(400).json({error: error.message})
})
});
/**
* PUT '/reject'
*/
RequestRouter.put('/reject', async (req:CustomJWTRequest, res:Response): Promise<Response> => {
const {request_id} = req.body;
const {token} = req;
return await requestManager.GetSingleRequest(request_id).then(result => {
if(result.TargetUser !== token){
throw new Error("Unauthorised")
} else {
return requestManager.RejectRequest(request_id).then((result) => {
return res.sendStatus(200)
}).catch((error: Error) => {
return res.status(400).json({error: error.message})
})
}
}).catch((error: Error) => {
return res.status(400).json({error: error.message})
})
});
import express, { Request, Response } from 'express'
export const IndexRouter = express.Router()
/**
* GET '/'
* Returns a string
*/
IndexRouter.get('/status', (req:Request, res:Response): void => {
res.json({'message': 'Service \'Friend-Service\' is running', statusCode: 200})
})
\ No newline at end of file
import express, { Application } from 'express'
import { authorize } from '../Middleware/Auth'
import { IndexRouter } from '../Routes'
import { FriendsRouter } from '../Routes/FriendsRouter'
import { RequestRouter } from '../Routes/RequestsRouter'
/**
* Load the application endpoints
* @param app
*/
export const initializeRoutes = (app: Application) => {
app.use(express.json())
// load index routes
app.use('/', IndexRouter)
//Other routes
app.use('/friends', authorize, FriendsRouter)
app.use('/friends/requests', authorize, RequestRouter)
}
\ No newline at end of file
export type Friend = {
User1: string,
User2: string
}
\ No newline at end of file
export type FriendRequest = {
SourceUser: string,
TargetUser: string
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment