diff --git a/.eslintrc.js b/.eslintrc.js index cf85bb7..88699d5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,5 +12,6 @@ module.exports = { 'no-console': 'off', 'import/prefer-default-export': 'off', '@typescript-eslint/no-unused-vars': 'warn', + 'prefer-spread': ['off'] }, }; \ No newline at end of file diff --git a/commands.sql b/commands.sql new file mode 100644 index 0000000..9872ae3 --- /dev/null +++ b/commands.sql @@ -0,0 +1,165 @@ +drop table if exists companies; +drop table if exists apps; +drop table if exists api_keys; +drop table if exists users; +drop table if exists channels; +drop table if exists messages; +drop table if exists user_app; +drop table if exists company_app; +drop table if exists api_key_app; +drop table if exists user_channel; +drop table if exists message_channel; +drop table if exists message_user; +drop table if exists developers; +drop table if exists company_developer; +drop table if exists developer_app; +drop table if exists api_key_developer; + +CREATE TABLE companies ( + id UUID PRIMARY KEY default uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + updated_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +CREATE TABLE apps ( + id UUID PRIMARY KEY default uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + updated_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +CREATE TABLE api_keys ( + id UUID PRIMARY KEY default uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + api_key VARCHAR(255) NOT NULL, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + updated_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +CREATE TABLE developers ( + id UUID PRIMARY KEY default uuid_generate_v4(), + username VARCHAR(255) NOT NULL, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + updated_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +CREATE TABLE users ( + id UUID PRIMARY KEY default uuid_generate_v4(), + username VARCHAR(255) NOT NULL, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + updated_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +CREATE TABLE channels ( + id UUID PRIMARY KEY default uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + owner_user_id UUID REFERENCES users(id) on delete set null, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + updated_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +CREATE TABLE messages ( + id UUID PRIMARY KEY default uuid_generate_v4(), + message TEXT NOT NULL, + created_at timestamp with time zone default timezone('utc'::text, now()) not null, + updated_at timestamp with time zone default timezone('utc'::text, now()) not null +); + +CREATE TABLE company_developer ( + id UUID PRIMARY KEY default uuid_generate_v4(), + company_id UUID REFERENCES companies(id) on delete cascade, + developer_owner_id UUID REFERENCES developers(id) on delete cascade +); + +CREATE TABLE developer_app ( + id UUID PRIMARY KEY default uuid_generate_v4(), + developer_id UUID REFERENCES developers(id) on delete cascade, + app_id UUID REFERENCES apps(id) on delete cascade +); + +CREATE TABLE company_app ( + id UUID PRIMARY KEY default uuid_generate_v4(), + company_id UUID REFERENCES companies(id) on delete cascade, + app_id UUID REFERENCES apps(id) on delete cascade +); +CREATE TABLE channel_app ( + id UUID PRIMARY KEY default uuid_generate_v4(), + channel_id UUID REFERENCES channels(id) on delete cascade, + app_id UUID REFERENCES apps(id) on delete cascade +); + +CREATE TABLE api_key_app ( + id UUID PRIMARY KEY default uuid_generate_v4(), + api_key_id UUID REFERENCES api_keys(id) on delete set null, + app_id UUID REFERENCES apps(id) on delete cascade +); + +CREATE TABLE api_key_developer ( + id UUID PRIMARY KEY default uuid_generate_v4(), + api_key_id UUID REFERENCES api_keys(id) on delete set null, + developer_id UUID REFERENCES developers(id) on delete cascade +); + +CREATE TABLE user_app ( + id UUID PRIMARY KEY default uuid_generate_v4(), + user_id UUID REFERENCES users(id) on delete set null, + app_id UUID REFERENCES apps(id) on delete cascade +); + +CREATE TABLE user_channel ( + id UUID PRIMARY KEY default uuid_generate_v4(), + user_id UUID REFERENCES users(id) on delete set null, + channel_id UUID REFERENCES channels(id) on delete cascade +); + +CREATE TABLE message_channel ( + id UUID PRIMARY KEY default uuid_generate_v4(), + message_id UUID REFERENCES messages(id) on delete set null, + channel_id UUID REFERENCES channels(id) on delete cascade +); + +CREATE TABLE message_user ( + id UUID PRIMARY KEY default uuid_generate_v4(), + message_id UUID REFERENCES messages(id) on delete set null, + user_id UUID REFERENCES users(id) on delete cascade +); + + +CREATE OR REPLACE FUNCTION trigger_set_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = timezone('utc'::text, now()); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON apps +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON api_keys +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON companies +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON users +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON channels +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON messages +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); \ No newline at end of file diff --git a/package.json b/package.json index 4ac6190..8125314 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,11 @@ "homepage": "https://github.com/nsmet/supabase-node-chat-backend#readme", "dependencies": { "@supabase/supabase-js": "^2.4.1", + "@types/jsonwebtoken": "^9.0.1", "body-parser": "^1.20.1", "dotenv": "^16.0.3", "express": "^4.18.2", + "jsonwebtoken": "^9.0.0", "socket.io": "^4.5.4" }, "devDependencies": { diff --git a/src/controllers/apps.controller.ts b/src/controllers/apps.controller.ts new file mode 100644 index 0000000..69043f1 --- /dev/null +++ b/src/controllers/apps.controller.ts @@ -0,0 +1,152 @@ +import { Response } from "express" +import { + TypedRequestBody, TypedRequestQueryWithParams, +} from '../types'; +import supabase from "../utils/supabase" + +export const createNewApp = async (req:TypedRequestBody<{name:string,developer_id:string}>, res:Response) => { + const name = req.body.name + const devID = req.body.developer_id + // TODO - need to get companyID myself from user. + const companyID = "0d871b21-03d6-4e75-873a-480d5ae097b9" + const appID = await addApp(name) + if(!appID) return res.sendStatus(500) + const companyAppID = await addCompanyApp(companyID,appID) + if(!companyAppID) return res.sendStatus(500) + const developerAppID = await addDeveloperApp(appID,devID) + if(!developerAppID) return res.sendStatus(500) + return res.send(appID) + } + +async function addApp(name:string):Promise{ + try{ + const { data, error } = await supabase + .from('apps') + .upsert({ + name:name + }) + .select() + if (error) { + console.log(error) + return null; + } else { + return data[0].id + } + } + catch(err){ + console.log(err) + return null + } +} +async function addCompanyApp(companyID:string,appID:string):Promise{ + try{ + const { data, error } = await supabase + .from('company_app') + .upsert({ + company_id:companyID, + app_id:appID + }) + .select() + if (error) { + console.log(error) + return null; + } else { + return data[0].id + } + }catch(err){ + console.log(err) + return null + } +} +async function addDeveloperApp(appID:string,devID:string):Promise{ + try{ + const { data, error } = await supabase + .from('developer_app') + .upsert({ + developer_id:devID, + app_id:appID + }) + .select() + if (error) { + console.log(error) + return null; + } else { + return data[0].id + } + }catch(err){ + console.log(err) + return null + } +} + + +export const deleteAppByID = async (req:TypedRequestQueryWithParams<{app_id:string}>, res:Response) => { + const appID = req.params.app_id + const deletedChannelApp = await removeChannelApp(appID) + if (!deletedChannelApp) return res.sendStatus(500) + const deletedCompanyApp = await removeCompanyApp(appID) + if (!deletedCompanyApp) return res.sendStatus(500) + const deletedDeveloperApp = await removeDeveloperApp(appID) + if (!deletedDeveloperApp) return res.sendStatus(500) + const deletedApp = await removeApp(appID) + if (!deletedApp) return res.sendStatus(500) + return res.send(deletedApp) +} + +const removeApp = async function(appID:string) { + const {error,data} = await supabase + .from('apps') + .delete() + .eq('id', appID) + .select() + if (error){ + console.log(error) + return null + }else { + console.log({data}) + return data + } +} +const removeChannelApp = async function(appID:string) { + const {error,data} = await supabase + .from('channel_app') + .delete() + .eq('app_id', appID) + .select() + if (error){ + console.log(error) + return null + }else { + console.log({data}) + return data + } +} +const removeCompanyApp = async function(appID:string) { + const {error,data} = await supabase + .from('company_app') + .delete() + .eq('app_id', appID) + .select() + if (error){ + console.log(error) + return null + }else { + console.log({data}) + return data + } +} +const removeDeveloperApp = async function(appID:string) { + const {error,data} = await supabase + .from('developer_app') + .delete() + .eq('app_id', appID) + .select() + if (error){ + console.log(error) + return null + }else { + console.log({data}) + return data + } +} + diff --git a/src/controllers/authentication.controller.ts b/src/controllers/authentication.controller.ts new file mode 100644 index 0000000..7895598 --- /dev/null +++ b/src/controllers/authentication.controller.ts @@ -0,0 +1,99 @@ +import { Response,Request } from "express" +import jwt from 'jsonwebtoken' +import { + TypedRequestBody, +} from '../types'; +import { newAPIKey,isValidAPIKey } from "../utils/auth"; +import supabase from "../utils/supabase"; + +export const getServerAPIKey = async (req:TypedRequestBody<{appID:string}>, res:Response) => { + // Get that from Supabase Auth + const devID = "b29c513b-4e9d-4479-8b36-c9eb9ca7f870" + // CompanyID should come from the users auth id? + const companyID = "d17f074e-baa9-4391-b24e-0c41f7944553" + const appID = req.body.appID + + if (!devID){ + return res.status(400).json({ error: 'No username!' }); + } + if (!appID){ + return res.status(400).json({ error: 'No appID!' }); + } + if (!companyID){ + return res.status(400).json({ error: 'No company!' }); + } + //TO DO need to do the validation on supabase side + //TO DO Need to put the key in a more secure place etc. and hash it + const apiKey = await newAPIKey({userID: devID,appID,companyID}) + if (!apiKey){ + return res.status(400).json({ error: 'Could not generate API key!' }); + } + return res.send(apiKey) + } + + async function getCompanyIDFromAppID (appID:string) { + try{ + console.log("looking up ",appID) + const { data, error } = await supabase + .from('company_app') + .select('company_id') + .eq('app_id', appID) + if (error){ + console.log(error) + return false + } + else{ + if (data.length === 0) { + throw new Error("No company id found") + } + if (data.length > 1) { + throw new Error("MORE THAN ONE APP found") + } + const companyID = data[0].company_id + return companyID + } + }catch(err){ + console.log(err) + throw new Error("Issue get") + } + } + + export const getChatToken = async (req: Request, res: Response) => { + const jwtKey = process.env.SECRET_JWT_KEY + if (!req.headers.authorization) { + return res.status(403).json({ error: 'No server key sent!' }); + } + if (!jwtKey) throw new Error("No JWT key found") + // Developer provides this: + // This is not the developer user, but the end user's username + const userID = req.body.user_id + // They send along app ID + const appID = req.body.app_id + + const companyID = await getCompanyIDFromAppID(appID) + console.log(companyID) + + if (!userID){ + return res.status(403).json({error:"No username"}) + } + if (!appID){ + return res.status(403).json({error:"No app ID"}) + } + if (!companyID){ + return res.status(403).json({error:"No team"}) + } + + const apikey = req.headers.authorization.split(' ')[1] + + const isValid = await isValidAPIKey(appID,apikey) + if (!isValid){ + return res.status(400).json({ error: 'Invalid API key' }); + } + + const claims = { userID,companyID,appID } + // To do - make async? + const token = jwt.sign(claims, jwtKey,{ + expiresIn:60000 // TO DO - need to make this longer & revokable and renewable + }) + return res.send(token) + } \ No newline at end of file diff --git a/src/controllers/channels.controller.ts b/src/controllers/channels.controller.ts new file mode 100644 index 0000000..5fbbb81 --- /dev/null +++ b/src/controllers/channels.controller.ts @@ -0,0 +1,314 @@ +import { Response, Request } from "express" +import supabase from "../utils/supabase" +import Socket from '../utils/socket'; +import { + TypedRequestBody, + TypedRequestQuery, + TypedRequestQueryAndParams, + TypedRequestQueryWithBodyAndParams +} from '../types'; +import { extractDataFromJWT } from "../utils/auth"; + +export const getAllChannels = async function (req: TypedRequestQuery<{user_id: string}>, res: Response) { + // get all channels this user is attached to + const paticipatingChannelIds = await supabase + .from('user_channel') + .select('channel_id') + .eq('user_id', req.query.user_id) + + if (!paticipatingChannelIds.data?.length) { + return res.send([]); + } + + const channels = await supabase + .from('channels') + .select(` + *, + messages ( + id, + channel_id, + message, + created_at, + users ( + id, + username + ) + ) + `) + .or(`owner_user_id.eq.${req.query.user_id},or(id.in.(${paticipatingChannelIds.data.map((item: any) => item.channel_id)}))`) + + return res.send(channels.data) +} + +export const createChannel = async function (req: TypedRequestBody<{participant_ids: string[], group_name: string}>, res: Response) { + + if (!req.body) return res.sendStatus(400) + if (!req.body.participant_ids || !req.body.group_name) return res.sendStatus(400) + if(!req.body.participant_ids.length) return res.sendStatus(400) + + const data = extractDataFromJWT(req as Request) + console.log(data) + + if (!data) return res.sendStatus(401); + const {userID,appID} = data + const { + participant_ids, + group_name, + } = req.body; + + // first create the channel + const channel = await addChannel(group_name,userID) + if (!channel) return res.sendStatus(500) + const channelID = channel[0].id + const channelApp = await addChannelToApp(channelID, appID) + + if (!channelApp) return res.sendStatus(500) + + const participantsInChannel = await addParticipantsToChannel(channelID, participant_ids) + if (!participantsInChannel) return res.sendStatus(500) + else return res.send(channel) + // TO DO - bring this back without errors + + // const participants: User[] = []; + // const conv: Channel = { + // ...channel[0], + // participants + // }; + + // Socket.notifyUsersOnChannelCreate(participant_ids as string[], conv) + // return res.send(conv); +} + +const addChannel = async function(name:string,userID:string) { + const channel = await supabase + .from('channels') + .upsert({ + name: name, + owner_user_id:userID + }) + .select() + + if (channel.error) { + return null + } + else return channel.data +} +const addChannelToApp = async function(channelID:string, appID:string) { + const channel = await supabase + .from('channel_app') + .upsert({ + channel_id: channelID, + app_id: appID + }) + .select() + + if (channel.error) { + return null + } + else return channel.data +} +const addParticipantsToChannel = async function(channelID:string, participantIDs:string[]) { + + try{ + await participantIDs.map(async paricipantID => { + console.log({paricipantID}) + const {data,error} = await supabase + .from('user_channel') + .upsert({ + user_id: paricipantID, + channel_id: channelID + }) + .select() + if (error) { + console.log(error) + return false + } + if (data){ + return true + } + }) + + }catch(err){ + console.log(err) + return false + } + +} + + +export const getChannelMessages = async function (req: TypedRequestQueryAndParams<{channel_id: string} ,{last_message_date: Date}>, res: Response) { + const { channel_id } = req.params; + const { last_message_date } = req.query; + + let query = supabase + .from('messages') + .select(` + id, + channel_id, + message, + created_at, + + users ( + id, + username + ) + `) + .order('created_at', { ascending: true }) + .eq('channel_id', channel_id) + + if (last_message_date){ + query = query.gt('created_at', last_message_date) + } + + const messages = await query; + + res.send(messages.data) +} +// this feels a bit crap +export const getChannelByID = async function (req: TypedRequestQueryAndParams<{channel_id: string} ,{last_message_date: Date}>, res: Response) { + // TODO - write this code + console.log('get channel by id') + const { channel_id } = req.params; + const { last_message_date } = req.query; + try { + const {error,data} = await supabase + .from('channels') + .select(` + name, + my_messages:message_channel( + id, + messages:messages( + id, + message, + who_sent:message_user( + user_id:users( + username + ) + ) + ) + ) + `) + // .order('created_at', { ascending: true }) + .eq('id', channel_id) + // TO DO - add messages onto query + // if (last_message_date){ + // query = query.gt('created_at', last_message_date) + // } + + if (error){ + console.log(error) + res.sendStatus(500) + } + else { + console.log(data) + const transformedData = transformData(data) + console.log(transformedData) + res.send(transformedData) + } + + // res.send(messages.data) + }catch(err){ + console.log(err) + res.sendStatus(500) + } + + +} + +export const updateChannelByID = async function (req: TypedRequestQueryWithBodyAndParams<{channel_id: string} ,{name:string;owner_user_id:string}>, res: Response) { + const { channel_id } = req.params; + const {name,owner_user_id} = req.body + const {error,data} = await supabase + .from('channels') + .update({name:name,owner_user_id:owner_user_id}) + .eq('id', channel_id) + .select() + if (error){ + console.log(error) + res.sendStatus(500) + }else { + console.log({data}) + res.send(data) + } + +} + + + +export const deleteChannelByID = async function (req: TypedRequestQueryAndParams<{channel_id: string} ,{last_message_date: Date}>, res: Response) { + const { channel_id:channelID } = req.params; + const deletedChannelMessages = await removeChannelMessage(channelID) + if (!deletedChannelMessages) return res.sendStatus(500) + if (deletedChannelMessages.length === 0){res.status(500).send("No matching channels");} + const deletedChannelApp = await removeChannelApp(channelID) + console.log({deletedChannelApp}) + if (!deletedChannelApp) return res.sendStatus(500) + const deletedChannel = await removeChannel(channelID) + console.log({deletedChannel}) + if (!deletedChannel) return res.sendStatus(500) + return res.send(deletedChannel) +} + +const removeChannel = async function(channelID:string) { + const {error,data} = await supabase + .from('channels') + .delete() + .eq('id', channelID) + .select() + if (error){ + console.log(error) + return null + }else { + console.log({data}) + return data + } +} +const removeChannelMessage = async function(channelID:string) { + const {error,data} = await supabase + .from('message_channel') + .delete() + .eq('channel_id', channelID) + .select() + if (error){ + console.log(error) + return null + }else { + console.log({data}) + return data + } +} +const removeChannelApp = async function(channelID:string) { + const {error,data} = await supabase + .from('channel_app') + .delete() + .eq('channel_id', channelID) + .select() + if (error){ + console.log(error) + return null + }else { + console.log({data}) + return data + } +} + + + +function transformData(dataReceived: any[]): any { + const transformedData = dataReceived.map((group) => { + return group.my_messages.map((message:any) => { + return { + name: group.name, + id: message.id, + messages: message.messages.message, + username: message.messages.who_sent[0].user_id.username, + }; + }); + }); + + // Flatten the transformed data into a single array + return [].concat.apply([], transformedData); + } + + +//function to transform dataReceived into desiredFormat diff --git a/src/controllers/companies.controller.ts b/src/controllers/companies.controller.ts new file mode 100644 index 0000000..bd39dd3 --- /dev/null +++ b/src/controllers/companies.controller.ts @@ -0,0 +1,32 @@ +import { Response } from "express" +import { + TypedRequestBody, +} from '../types'; +import supabase from "../utils/supabase" + +export const createNewCompany = async (req:TypedRequestBody<{name:string,user_id:string}>, res:Response) => { + const name = req.body.name + const userID = req.body.user_id + + // create new app in supabase + try { + const { data, error } = await supabase + .from('companies') + .upsert({ + name: name, + owner_user_id: userID + }) + .select() + if (error) { + console.log(error) + return res.status(500).json({error:"Could not create company"}) + } else { + console.log(data) + res.send(data[0]) + } + }catch(err) { + console.log(err) + return res.status(500).json({error:"Could not create company"}) + } + } + \ No newline at end of file diff --git a/src/controllers/conversation.controller.ts b/src/controllers/conversation.controller.ts deleted file mode 100644 index b5a4851..0000000 --- a/src/controllers/conversation.controller.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Response } from "express" -import supabase from "../utils/supabase" -import Socket from '../utils/socket'; -import { - TypedRequestBody, - TypedRequestQuery, - TypedRequestQueryWithBodyAndParams, - TypedRequestQueryAndParams, - User, - Message, - Conversation -} from '../types'; - -export const getAllConversations = async function (req: TypedRequestQuery<{user_id: string}>, res: Response) { - // get all conversations this user is attached to - const paticipatingConversationIds = await supabase - .from('user_conversation') - .select('conversation_id') - .eq('user_id', req.query.user_id) - - if (!paticipatingConversationIds.data?.length) { - return res.send([]); - } - - const conversations = await supabase - .from('conversations') - .select(` - *, - messages ( - id, - conversation_id, - message, - created_at, - users ( - id, - username - ) - ) - `) - .or(`owner_user_id.eq.${req.query.user_id},or(id.in.(${paticipatingConversationIds.data.map((item: any) => item.conversation_id)}))`) - - return res.send(conversations.data) -} - -export const createConversation = async function (req: TypedRequestBody<{owner_id: string, participant_ids: string[], group_name: string}>, res: Response) { - const { - owner_id, - participant_ids, - group_name, - } = req.body; - - // first create the conversation - const conversation = await supabase - .from('conversations') - .upsert({ - name: group_name, - owner_user_id: owner_id, - created_at: ((new Date()).toISOString()).toLocaleString() - }) - .select() - - if (conversation.error) { - res.send(500) - } - - let participants: User[] = []; - - if (participant_ids.length > 1 && conversation.data?.length) { - // attach all our users to this conversation - const pivotData = await supabase - .from('user_conversation') - .upsert(participant_ids.map((participant_id) => { - return { - user_id: participant_id, - conversation_id: conversation.data[0].id - } - })) - .select() - - if (pivotData.data?.length) { - // find our actual users - const actualParticipantUsers = await supabase - .from('users') - .select() - .in('id', participant_ids) - - if (actualParticipantUsers.data?.length) participants = actualParticipantUsers.data; - } - } - - if (conversation.error) { - return res.sendStatus(500) - } else { - const conv: Conversation = { - ...conversation.data[0], - participants - }; - - Socket.notifyUsersOnConversationCreate(participant_ids as string[], conv) - return res.send(conv); - } -} - -export const addMessageToConversation = async function (req: TypedRequestQueryWithBodyAndParams<{conversation_id: string}, {user_id: string, message: string}>, res: Response) { - const conversationid = req.params.conversation_id; - const { - user_id, - message, - } = req.body; - - const data = await supabase - .from('messages') - .upsert({ - conversation_id: conversationid, - user_id, - message, - created_at: ((new Date()).toISOString()).toLocaleString() - }) - .select(` - *, - users ( - id, - username - ), - conversations (*) - `) - - // get the users in this chat, except for the current one - const userConversationIds = await supabase - .from('user_conversation') - .select('user_id') - .eq('conversation_id', conversationid) - - if (data.error) { - res.send(500) - } else { - if (userConversationIds.data && userConversationIds.data?.length > 0) { - const userIdsForMessages = userConversationIds.data.map((item) => item.user_id).filter((item) => item !== user_id); - Socket.sendMessageToUsers(userIdsForMessages as string[], data.data[0] as Message) - } - - res.send( - data.data[0] - ) - } -} - -export const getConversationMessages = async function (req: TypedRequestQueryAndParams<{conversation_id: string} ,{last_message_date: Date}>, res: Response) { - const { conversation_id } = req.params; - const { last_message_date } = req.query; - - let query = supabase - .from('messages') - .select(` - id, - conversation_id, - message, - created_at, - - users ( - id, - username - ) - `) - .order('created_at', { ascending: true }) - .eq('conversation_id', conversation_id) - - if (last_message_date){ - query = query.gt('created_at', last_message_date) - } - - const messages = await query; - - res.send(messages.data) -} \ No newline at end of file diff --git a/src/controllers/developers.controller.ts b/src/controllers/developers.controller.ts new file mode 100644 index 0000000..737372a --- /dev/null +++ b/src/controllers/developers.controller.ts @@ -0,0 +1,70 @@ +import { Response } from "express" +import supabase from "../utils/supabase" +import { TypedRequestBody } from "../types" + +export const createDeveloper = async function (req: TypedRequestBody<{username: string}>, res: Response) { + const username = req.body.username + const devID = await addDeveloper(username) + if(!devID) return res.sendStatus(500) + const companyID = await addCompany(username) + if(!companyID) return res.sendStatus(500) + const companyDeveloperID = await addCompanyDeveloper(companyID,devID) + if(!companyDeveloperID) return res.sendStatus(500) + return res.send(devID) +} + +async function addDeveloper(username:string):Promise{ + try{ + const { data, error } = await supabase + .from('developers') + .upsert({ + username:username + }) + .select() + if (error) { + console.log(error) + return null; + } else { + return data[0].id + } + } + catch(err){ + console.log(err) + return null + } +} +async function addCompany(username:string):Promise{ + try{ + const { data, error } = await supabase + .from('companies') + .upsert({ + name:`${username} Co` + }) + .select() + if (error) { + return null; + } else { + return data[0].id + } + }catch(err){ + return null + } +} +async function addCompanyDeveloper(companyID:string,developerID:string):Promise{ + try{ + const { data, error } = await supabase + .from('company_developer') + .upsert({ + company_id:companyID, + developer_owner_id:developerID + }) + .select() + if (error) { + return null; + } else { + return data[0].id + } + }catch(err){ + return null + } +} diff --git a/src/controllers/messages.controller.ts b/src/controllers/messages.controller.ts new file mode 100644 index 0000000..f48a403 --- /dev/null +++ b/src/controllers/messages.controller.ts @@ -0,0 +1,224 @@ +import { Response } from "express" +import supabase from "../utils/supabase" +import Socket from '../utils/socket'; +import { + TypedRequestQueryWithParams, + Message, + TypedRequestBody, + TypedRequestQuery, + TypedRequestQueryWithBodyAndParams +} from '../types'; + + +export const sendMessageToChannel = async function (req: TypedRequestBody<{user_id: string, message: string,channel_id:string}>, res: Response) { + const { + channel_id, + user_id, + message, + } = req.body; + + const messageID = await addMessage(message) + if (!messageID){return res.sendStatus(500)} + console.log(messageID) + const messageUserID = await addMessageToUser(messageID,user_id) + console.log(messageUserID) + if (!messageUserID){return res.sendStatus(500)} + console.log({channel_id}) + const messageChannelID = await addMessageToChannel(messageID,channel_id) + console.log(messageChannelID) + return res.send(messageID) + // add message to user + // add message to channel + + // TO DO - fix up the socket io stuff + + // get the users in this chat, except for the current one + // const userChannelIds = await supabase + // .from('user_channel') + // .select('user_id') + // .eq('channel', channelID) + // To Do: get this working again + // if (data.error) { + // res.send(500) + // } else { + // if (userChannelIds.data && userChannelIds.data?.length > 0) { + // const userIdsForMessages = userChannelIds.data.map((item) => item.user_id).filter((item) => item !== user_id); + // Socket.sendMessageToUsers(userIdsForMessages as string[], data.data[0] as Message) + // } + + // res.send( + // data.data[0] + // ) + // } +} +const addMessage = async function (message:string) { + try{ + const {error,data} = await supabase + .from('messages') + .upsert({ + message, + }) + .select(` + id + `) + if (error){ + console.log(error) + return null + } + console.log(data) + return data[0].id + }catch(err){ + console.log(err) + return null + } +} +const addMessageToUser = async function (messageID:string, userID:string) { + try{ + const {error,data} = await supabase + .from('message_user') + .upsert({ + message_id:messageID, + user_id:userID + }) + .select(` + id + `) + if (error){ + console.log(error) + return null + } + console.log(data) + return data[0].id + }catch(err){ + console.log(err) + return null + } +} +const addMessageToChannel = async function (messageID:string, channelID:string) { + try{ + const {error,data} = await supabase + .from('message_channel') + .upsert({ + message_id:messageID, + channel_id:channelID + }) + .select(` + id + `) + if (error){ + console.log(error) + return null + } + console.log(data) + return data[0].id + }catch(err){ + console.log(err) + return null + } +} + + + + +export const getMessageByID = async function (req:TypedRequestQueryWithParams <{message_id: string}>, res: Response) { + const messageID = req.params.message_id + + const { data, error } = await supabase + .from('messages') + .select(`*, sender:message_user(username:users(username))`) + .eq('id', messageID) + + if (error) { + console.log(error) + res.sendStatus(500) + } else { + const message = data[0] + const sender = message.sender as {username:{username:string}}[] + const username = sender[0].username.username + return res.send({ + id:message.id, + message:message.message, + created_at:message.created_at, + updated_at:message.updated_at, + username + }) + + } +} + +export const updateMessageByID = async function (req: TypedRequestQueryWithBodyAndParams<{message_id: string},{new_message:string}>, res: Response) { + const messageID = req.params.message_id + const newMessage = req.body.new_message + const { data, error } = await supabase + .from('messages') + .update({ + message: newMessage + }) + .eq('id', messageID) + .select() + if (error) { + console.log(error) + return res.sendStatus(500) + } else { + return res.send(data[0]) + } +} + +export const deleteMessageByID = async function (req: TypedRequestQueryWithParams<{message_id: string}>, res: Response) { + const message_id = req.params.message_id + const removedMessageUser = await removeMessageUser(message_id) + if (!removedMessageUser){return res.sendStatus(500)} + const removedMessageChannel = await removeMessageChannel(message_id) + if (!removedMessageChannel){return res.sendStatus(500)} + const deletedMessage = await removeMessage(message_id) + if (!deletedMessage){return res.sendStatus(500)} + return res.send(deletedMessage) + + // TODO - write this code + +} +const removeMessageUser = async function (messageID:string) { + const { data, error } = await supabase + .from('message_user') + .delete() + .eq('message_id', messageID) + .select() + if (error) { + console.log(error) + return null + } else { + console.log(data) + return data[0] + } +} +const removeMessageChannel = async function (messageID:string) { + const { data, error } = await supabase + .from('message_channel') + .delete() + .eq('message_id', messageID) + .select() + if (error) { + console.log(error) + return null + } else { + console.log("success") + console.log(data) + return data[0] + } +} + +const removeMessage = async function (messageID:string) { + const { data, error } = await supabase + .from('messages') + .delete() + .eq('id', messageID) + .select() + if (error) { + console.log(error) + return null + } else { + console.log("success") + console.log(data) + return data[0] + } +} \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts deleted file mode 100644 index a84f8d9..0000000 --- a/src/controllers/user.controller.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Response } from "express" -import supabase from "../utils/supabase" -import { TypedRequestBody, TypedRequestQuery } from "../types" - -export const createUser = async function (req: TypedRequestBody<{username: string}>, res: Response) { - const { data, error } = await supabase - .from('users') - .upsert({ - username: req.body.username, - created_at: ((new Date()).toISOString()).toLocaleString() - }) - .select() - - if (error) { - res.send(500) - } else { - res.send(data[0]) - } -} - -export const searchUsers = async function (req: TypedRequestQuery<{user_id: string, q: string}>, res: Response) { - - let query = supabase - .from('users') - .select(); - - if (req.query.q) { - query = query.like('username', `%${req.query.q}%`) - } - - query = query.neq('id', req.query.user_id) - .limit(50); - - const { data, error } = await query; - - if (error) { - res.send(500) - } else { - res.send(data) - } -} \ No newline at end of file diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts new file mode 100644 index 0000000..bfb5d4d --- /dev/null +++ b/src/controllers/users.controller.ts @@ -0,0 +1,216 @@ +import { Response,Request } from "express" +import supabase from "../utils/supabase" +import { TypedRequestBody, TypedRequestQuery, TypedRequestQueryWithParams } from "../types" +import { extractDataFromJWT } from "../utils/auth" + +export const createUser = async function (req: TypedRequestBody<{username: string;app_id:string}>, res: Response) { + // TO DO - need app ID from JWT + + const userID = await addUser(req.body.username) + if (!userID) return res.sendStatus(500) + const appUserID = await addUserToApp(userID,req.body.app_id) + if (!appUserID) return res.sendStatus(500) + return res.send(userID) +} +const addUser = async function(username:string){ + const { data, error } = await supabase + .from('users') + .upsert({ + username: username, + }) + .select() + if (error) { + return null + } else { + return data[0].id + } +} +const addUserToApp = async function(userID:string,appID:string){ + const { data, error } = await supabase + .from('user_app') + .upsert({ + user_id: userID, + app_id: appID + }) + .select() + if (error) { + return null + } else { + return data[0].id + } +} + +// Create a new user_app entry +// create user_app entry + +export const getAllUsers = async function (req: Request, res: Response) { + const dataFromJWT = extractDataFromJWT(req as Request) + console.log(dataFromJWT) + if (!dataFromJWT) return res.sendStatus(401); + const {appID} = dataFromJWT + const { data, error } = await supabase + .from('user_app') + .select(`users(*)`) + .eq('app_id', appID) + + if (error) { + return res.sendStatus(500) + } else { + return res.send(data) + } +} +export const getUserByID = async function (req: TypedRequestQueryWithParams<{user_id: string}>, res: Response) { + const userID = req.params.user_id + const { data, error } = await supabase + .from('users') + .select() + .eq('id', userID) + if (error) { + res.sendStatus(500) + } else { + res.send(data[0]) + } +} +export const updateCurrentUser = async function (req: TypedRequestBody<{new_username: string}>, res: Response) { + // Note these are for updating the current user + // If we want to update a user by ID, we need an admin route + const dataFromJWT = extractDataFromJWT(req as Request) + if (!dataFromJWT) return res.sendStatus(401); + const {userID} = dataFromJWT + + const newUsername = req.body.new_username + + try { + const { data, error } = await supabase + .from('users') + .update({ + username: newUsername + }) + .eq('id', userID) + .select() + + if (error) { + console.log(error) + return res.sendStatus(400) + } else { + console.log(data) + + return res.sendStatus(200) + } + }catch(err){ + console.log(err) + return res.sendStatus(401) + } + +} +export const deleteUserByID = async function (req: TypedRequestQueryWithParams<{user_id: string}>, res: Response) { + console.log("came in to delete") + console.log(req.params) + const userID = req.params.user_id + console.log({userID}) + try{ + const { data, error } = await supabase + .from('users') + .delete() + .eq('id', userID) + if (error) { + console.log(error) + return res.sendStatus(500) + } else { + return res.send(data[0]) + } + }catch(err){ + console.log(err) + return res.sendStatus(500) + } +} +export const deleteCurrentUser = async function (req:Request, res: Response) { + const dataFromJWT = extractDataFromJWT(req as Request) + if (!dataFromJWT) return res.sendStatus(401); + const {userID} = dataFromJWT + + const removedFromApp = await removeUserFromApp(userID) + if (!removedFromApp) return res.sendStatus(500) + const removedUser = await removeUser(userID) + if (!removedUser) return res.sendStatus(500) + return res.sendStatus(200) +} +const removeUser = async function(userID:string){ + try{ + const { data, error } = await supabase + .from('users') + .delete() + .eq('id', userID) + .select() + if (error) { + console.log(error) + return null + } else { + return data[0] + } + }catch(err){ + console.log(err) + return null + } +} +const removeUserFromApp = async function(userID:string){ + try{ + const { data, error } = await supabase + .from('user_app') + .delete() + .eq('user_id', userID) + .select() + if (error) { + console.log(error) + return null + } else { + console.log(data) + return data[0] + } + }catch(err){ + console.log(err) + return null + } +} + + + +export const searchUsers = async function (req: TypedRequestQuery<{user_id: string, q: string}>, res: Response) { + // To do - make sure matches what I put in api + // To do - make sure they are in the same app + let query = supabase + .from('users') + .select(); + + if (req.query.q) { + query = query.like('username', `%${req.query.q}%`) + } + + query = query.neq('id', req.query.user_id) + .limit(50); + + const { data, error } = await query; + + if (error) { + return res.sendStatus(500) + } else { + return res.send(data) + } +} + +export const connectUser = async function (req: TypedRequestBody<{username: string}>, res: Response) { + // TODO - write this code + const { data, error } = await supabase + .from('users') + .upsert({ + username: req.body.username, + created_at: ((new Date()).toISOString()).toLocaleString() + }) + .select() + + if (error) { + return res.sendStatus(500) + } else { + return res.send(data[0]) + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4ca3a93..9057230 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,26 @@ -import express from "express"; +import express, { NextFunction, Request, Response } from "express"; import bodyParser from "body-parser"; import cors from 'cors'; import http from 'http'; import { Server } from 'socket.io'; -import { createUser, searchUsers } from './controllers/user.controller'; +import { createUser, searchUsers, getAllUsers, getUserByID, updateCurrentUser, deleteUserByID, connectUser, deleteCurrentUser } from './controllers/users.controller'; import Socket from "./utils/socket"; import { - createConversation, - addMessageToConversation, - getAllConversations, - getConversationMessages -} from './controllers/conversation.controller'; + createChannel as createChannel, + deleteChannelByID, + getAllChannels, + getChannelByID, + getChannelMessages as getMessagesInAChannel, + updateChannelByID +} from './controllers/channels.controller'; +import { deleteMessageByID, getMessageByID, sendMessageToChannel, updateMessageByID } from "./controllers/messages.controller"; +import { getServerAPIKey,getChatToken } from "./controllers/authentication.controller"; +import { secureClientRoutesWithJWTs } from "./utils/auth"; +import { createNewApp,deleteAppByID } from "./controllers/apps.controller"; +import { createNewCompany } from "./controllers/companies.controller"; +import { createDeveloper } from "./controllers/developers.controller"; const app = express(); const server = http.createServer(app); @@ -23,20 +31,57 @@ app.use(bodyParser.urlencoded({ extended: false})); app.use(bodyParser.json()); app.use(cors()) + +app.use(secureClientRoutesWithJWTs); + app.get("/", function (req, res) { - res.send("Hello World"); + return res.send("Thanks for using our chat API. Please check out our docs"); }); - + +// DEVELOPERS ENDPOINTS +app.post("/developers", createDeveloper) // done + +// AUTHENTICATION ENDPOINTS +app.get("/get-server-api-key", getServerAPIKey); // done +app.get("/get-chat-token",getChatToken); // done (with some checks needed) + // USER ENDPOINTS -app.post("/users/create", createUser); -app.get("/users/search", searchUsers); +app.get("/users", getAllUsers); // Think its working +app.post("/users", createUser); // working +app.put("/users", updateCurrentUser ); // working +app.get("/users/:user_id", getUserByID); // working +app.delete("/users", deleteCurrentUser); // working (need to double check though) + +// TO DO - need to do these ones +// app.delete("/users/:user_id",deleteUserByID); +// app.put("/users/:user_id",updateUserByID); + +// CHANNEL ENDPOINTS +app.post("/channels", createChannel); // working +app.get("/channels/:channel_id", getChannelByID); // kind of working except messages +app.put("/channels/:channel_id", updateChannelByID); // done +app.delete("/channels/:channel_id", deleteChannelByID); // done +app.get("/channels", getAllChannels) // need to make changes +app.get("/channels/:channel_id/messages", getMessagesInAChannel) + +app.post("/channels/:channel_id/users/:user_id", () => console.log("join a channel")); + +// Messages +app.post("/messages", sendMessageToChannel) // done +app.get("/messages/:message_id",getMessageByID); // done +app.put("/messages/:message_id", updateMessageByID); // done +app.delete("/messages/:message_id", deleteMessageByID); // done + +// App +app.post("/apps",createNewApp) // done +app.delete("/apps/:app_id", deleteAppByID); // need to do + +// Companies +app.post("/companies", createNewCompany); // dont need now -// CONVERSATION ENDPOINTS -app.post("/conversations/create", createConversation); -app.get("/conversations", getAllConversations) -app.get("/conversations/:conversation_id/messages", getConversationMessages) +// Unclear parts +app.post("/users/connect", connectUser); +app.get("/users/search?q=:query", searchUsers); -// SEND A MESSAGE -app.post("/conversations/:conversation_id/messages/create", addMessageToConversation) server.listen(3000); \ No newline at end of file diff --git a/src/supabase.types.ts b/src/supabase.types.ts index fe24fbd..1120718 100644 --- a/src/supabase.types.ts +++ b/src/supabase.types.ts @@ -9,62 +9,298 @@ export type Json = export interface Database { public: { Tables: { - conversations: { + api_key_app: { Row: { + api_key_id: string | null + app_id: string | null + id: string + } + Insert: { + api_key_id?: string | null + app_id?: string | null + id?: string + } + Update: { + api_key_id?: string | null + app_id?: string | null + id?: string + } + } + api_key_developer: { + Row: { + api_key_id: string | null + developer_id: string | null + id: string + } + Insert: { + api_key_id?: string | null + developer_id?: string | null + id?: string + } + Update: { + api_key_id?: string | null + developer_id?: string | null + id?: string + } + } + api_keys: { + Row: { + api_key: string created_at: string id: string name: string - owner_user_id: string + updated_at: string } Insert: { + api_key: string + created_at?: string + id?: string + name: string + updated_at?: string + } + Update: { + api_key?: string + created_at?: string + id?: string + name?: string + updated_at?: string + } + } + apps: { + Row: { created_at: string + id: string + name: string + updated_at: string + } + Insert: { + created_at?: string id?: string name: string - owner_user_id: string + updated_at?: string } Update: { created_at?: string id?: string name?: string - owner_user_id?: string + updated_at?: string } } - messages: { + channel_app: { + Row: { + app_id: string | null + channel_id: string | null + id: string + } + Insert: { + app_id?: string | null + channel_id?: string | null + id?: string + } + Update: { + app_id?: string | null + channel_id?: string | null + id?: string + } + } + channels: { Row: { - conversation_id: string | null created_at: string id: string - message: string + name: string + owner_user_id: string | null + updated_at: string + } + Insert: { + created_at?: string + id?: string + name: string + owner_user_id?: string | null + updated_at?: string + } + Update: { + created_at?: string + id?: string + name?: string + owner_user_id?: string | null + updated_at?: string + } + } + companies: { + Row: { + created_at: string + id: string + name: string + updated_at: string + } + Insert: { + created_at?: string + id?: string + name: string + updated_at?: string + } + Update: { + created_at?: string + id?: string + name?: string + updated_at?: string + } + } + company_app: { + Row: { + app_id: string | null + company_id: string | null + id: string + } + Insert: { + app_id?: string | null + company_id?: string | null + id?: string + } + Update: { + app_id?: string | null + company_id?: string | null + id?: string + } + } + company_developer: { + Row: { + company_id: string | null + developer_owner_id: string | null + id: string + } + Insert: { + company_id?: string | null + developer_owner_id?: string | null + id?: string + } + Update: { + company_id?: string | null + developer_owner_id?: string | null + id?: string + } + } + developer_app: { + Row: { + app_id: string | null + developer_id: string | null + id: string + } + Insert: { + app_id?: string | null + developer_id?: string | null + id?: string + } + Update: { + app_id?: string | null + developer_id?: string | null + id?: string + } + } + developers: { + Row: { + created_at: string + id: string + updated_at: string + username: string + } + Insert: { + created_at?: string + id?: string + updated_at?: string + username: string + } + Update: { + created_at?: string + id?: string + updated_at?: string + username?: string + } + } + message_channel: { + Row: { + channel_id: string | null + id: string + message: string | null + } + Insert: { + channel_id?: string | null + id?: string + message?: string | null + } + Update: { + channel_id?: string | null + id?: string + message?: string | null + } + } + message_user: { + Row: { + id: string + message: string | null user_id: string | null } Insert: { - conversation_id?: string | null + id?: string + message?: string | null + user_id?: string | null + } + Update: { + id?: string + message?: string | null + user_id?: string | null + } + } + messages: { + Row: { created_at: string + id: string + message: string + updated_at: string + } + Insert: { + created_at?: string id?: string message: string - user_id?: string | null + updated_at?: string } Update: { - conversation_id?: string | null created_at?: string id?: string message?: string + updated_at?: string + } + } + user_app: { + Row: { + app_id: string | null + id: string + user_id: string | null + } + Insert: { + app_id?: string | null + id?: string + user_id?: string | null + } + Update: { + app_id?: string | null + id?: string user_id?: string | null } } - user_conversation: { + user_channel: { Row: { - conversation_id: string | null + channel_id: string | null id: string user_id: string | null } Insert: { - conversation_id?: string | null + channel_id?: string | null id?: string user_id?: string | null } Update: { - conversation_id?: string | null + channel_id?: string | null id?: string user_id?: string | null } @@ -73,16 +309,19 @@ export interface Database { Row: { created_at: string id: string + updated_at: string username: string } Insert: { - created_at: string + created_at?: string id?: string + updated_at?: string username: string } Update: { created_at?: string id?: string + updated_at?: string username?: string } } @@ -96,5 +335,8 @@ export interface Database { Enums: { [_ in never]: never } + CompositeTypes: { + [_ in never]: never + } } } diff --git a/src/types.ts b/src/types.ts index e09244e..ee474f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,12 @@ import { Socket } from "socket.io" export interface TypedRequestBody extends Express.Request { body: T } +export interface TypedRequestBodyWithHeader extends Express.Request { + body: T + headers:{ + authorization:string + } + } export interface TypedRequestQuery extends Express.Request { query: T @@ -28,11 +34,12 @@ export interface User { created_at: string; } -export interface Conversation { +export interface Channel { id: string; name: string; - owner_user_id: string; + owner_user_id: string | null; created_at: string; + updated_at:string; participants?: User[]; } @@ -43,9 +50,9 @@ export interface Message { created_at: string; } -export interface UserConversation { +export interface UserChannel { user_id: string; - conversation_id: string; + channel_id: string; } export interface SocketConnectedUsers { @@ -58,4 +65,12 @@ export interface SocketConnectedUsers { export interface SocketSocketIdUserId { [key: string]: string -} \ No newline at end of file +} + +export interface UserPayLoad { + userID:string; + companyID:string; + appID:string; + iat:number; + exp:number + } \ No newline at end of file diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..568f0d6 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,172 @@ +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken" +import crypto from 'crypto' + +import { UserPayLoad } from "../types"; +import supabase from "../utils/supabase" + +function verifyToken(token:string) { + const jwtKey = process.env.SECRET_JWT_KEY + if (!jwtKey) throw new Error("No JWT key found") + if (!token) return false + try{ + const {userID,companyID,appID} = jwt.verify(token,jwtKey) as UserPayLoad + if (!companyID){ + return false + } + if (!userID){ + return false + } + if (!appID){ + return false + } + return true + }catch(err){ + return false + } + + } + + +export function extractDataFromJWT (req:Request) { + if (!req.headers.authorization) throw new Error("No JWT found") + const token = req.headers.authorization.split(' ')[1] + const jwtKey = process.env.SECRET_JWT_KEY + if (!jwtKey) throw new Error("No JWT key found") + if (!token) return false + + + const {userID,companyID,appID} = jwt.verify(token,jwtKey) as UserPayLoad + return ({userID,companyID,appID}) +} + +export const secureClientRoutesWithJWTs = async (req:Request, res:Response, next:NextFunction) =>{ + // TO DO - need to make sure these routes are secure + const nonSecureRoutes = ['/get-chat-token','/get-server-api-key',"/apps","/companies","/developers"] + console.log(req.path.split("/")) + const initialPath = req.path.split("/")[1] + if (initialPath === "apps" && req.method === "DELETE"){ + return next() + } + // TO DO - need to put this behind API key & server only. + if (req.path === "/users" && req.method === "POST"){ + return next() + } + + if (nonSecureRoutes.includes(req.path)){ + return next() + } + if (!req.headers.authorization) { + return res.status(403).json({ error: 'No credentials sent!' }); + } + const jwt = req.headers.authorization.split(' ')[1] + const isVerified = verifyToken(jwt) + + if (!isVerified){ + return res.status(401).json({ error: 'Invalid token' }); + } + next(); + } + +export const newAPIKey = async function (keyDetails:{userID:string,appID:string,companyID:string}) { + const {userID, appID} = keyDetails + const newKey = crypto.randomUUID(); + const APIKeyID = await addAPIKey(newKey) + if (!APIKeyID){ + return null + } + const APIKeyToDeveloperID = await linkAPIKeyToDeveloper(APIKeyID,userID) + if (!APIKeyToDeveloperID){ + return null + } + const APIKeyToApp = await linkAPIKeyToApp(APIKeyID,appID) + if (!APIKeyToApp){ + return null + } + return newKey +} + +const addAPIKey = async function(newKey:string):Promise{ + + try{ + const {data, error } = await supabase + .from('api_keys') + .upsert({ api_key: newKey,name:"new API key"}) + .select() + if (error){ + console.log(error) + return null + } + else{ + console.log(data) + return data[0].id + } +}catch(err){ + console.log(err) + return null +} +} +const linkAPIKeyToDeveloper = async function(apiKeyID:string,developerID:string):Promise{ + try{ + const {data, error } = await supabase + .from('api_key_developer') + .upsert({ api_key_id: apiKeyID,developer_id:developerID}) + .select() + if (error){ + console.log(error) + return null + } + else{ + console.log(data) + return data[0].id + } +}catch(err){ + console.log(err) + return null +} +} + +const linkAPIKeyToApp = async function(apiKeyID:string,appID:string):Promise{ + try{ + const {data, error } = await supabase + .from('api_key_app') + .upsert({ api_key_id: apiKeyID,app_id:appID}) + .select() + if (error){ + console.log(error) + return null + } + else{ + console.log(data) + return data[0].id + } +}catch(err){ + console.log(err) + return null +} +} + +export const isValidAPIKey = async function (appID:string, receivedAPIKey:string) { + try{ + const { data, error } = await supabase + .from('api_key_app') + .select(`api_keys( + api_key + )`) + .eq('app_id', appID) + if (error){ + console.log(error) + return false + } + else{ + if (data.length === 0) { + console.log("No api key found") + return false + } + const isAuthenticated = data.some((item:any) => item.api_keys.api_key === receivedAPIKey) + return isAuthenticated + } + }catch(err){ + return false + } +} \ No newline at end of file diff --git a/src/utils/socket.ts b/src/utils/socket.ts index aa710af..2bbf2ec 100644 --- a/src/utils/socket.ts +++ b/src/utils/socket.ts @@ -1,5 +1,5 @@ import { Server } from "socket.io"; -import { SocketConnectedUsers, SocketSocketIdUserId, User, Message, Conversation } from '../types'; +import { SocketConnectedUsers, SocketSocketIdUserId, User, Message, Channel } from '../types'; class Socket { private static _instance: Socket; @@ -45,12 +45,12 @@ class Socket { }) } - public static notifyUsersOnConversationCreate (userIds: string[], conversation: Conversation) { + public static notifyUsersOnChannelCreate (userIds: string[], channel: Channel) { userIds.forEach((item) => { const user = this._instance.users[item]; if (user) { - user.socket.emit('newConversation', conversation); + user.socket.emit('newChannel', channel); } }) } diff --git a/yarn.lock b/yarn.lock index 1451c60..3cc0684 100644 --- a/yarn.lock +++ b/yarn.lock @@ -206,6 +206,13 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/jsonwebtoken@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz#29b1369c4774200d6d6f63135bf3d1ba3ef997a4" + integrity sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw== + dependencies: + "@types/node" "*" + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -453,6 +460,11 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + bufferutil@^4.0.1: version "4.0.7" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" @@ -642,6 +654,13 @@ dotenv@^16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -1181,6 +1200,33 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +jsonwebtoken@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1201,6 +1247,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -1518,7 +1569,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.2.1: +safe-buffer@5.2.1, safe-buffer@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -1533,7 +1584,7 @@ semver@^5.7.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^7.3.7: +semver@^7.3.7, semver@^7.3.8: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==