From e7de875ee4d8d7d7ed4ff73e29e410f2e40441e1 Mon Sep 17 00:00:00 2001 From: Jack Bridger Date: Thu, 9 Feb 2023 18:20:46 +0000 Subject: [PATCH 1/5] added authentication routes --- package.json | 2 + src/controllers/authentication.controller.ts | 45 +++++++++++ src/index.ts | 12 ++- src/supabase.types.ts | 3 + src/types.ts | 15 +++- src/utils/auth.ts | 84 ++++++++++++++++++++ yarn.lock | 55 ++++++++++++- 7 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 src/controllers/authentication.controller.ts create mode 100644 src/utils/auth.ts 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/authentication.controller.ts b/src/controllers/authentication.controller.ts new file mode 100644 index 0000000..6ed72eb --- /dev/null +++ b/src/controllers/authentication.controller.ts @@ -0,0 +1,45 @@ +import { Response,Request } from "express" +import jwt from 'jsonwebtoken' +import { + TypedRequestBody, +} from '../types'; +import { newAPIKey,isValidAPIKey } from "../utils/auth"; + + + + +export const getServerAPIKey = async (req:TypedRequestBody<{username:string}>, res:Response) => { + const username = req.body.username + if (!username){ + return res.status(400).json({ error: 'No username sent!' }); + } + //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(username) + return res.send(apiKey) + } + + 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") + const username = req.body.username + if (!username){ + return res.status(403).json({error:"No username"}) + } + const apikey = req.headers.authorization.split(' ')[1] + + const isValid = await isValidAPIKey(username,apikey) + if (!isValid){ + return res.status(400).json({ error: 'Invalid API key' }); + } + + const team = "tinder"; + const claims = { team,username } + const token = await jwt.sign(claims, jwtKey,{ + expiresIn:60 + }) + return res.send(token) + } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4ca3a93..d937b60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import express from "express"; +import express, { NextFunction, Request, Response } from "express"; import bodyParser from "body-parser"; import cors from 'cors'; import http from 'http'; @@ -13,6 +13,8 @@ import { getAllConversations, getConversationMessages } from './controllers/conversation.controller'; +import { getServerAPIKey,getChatToken } from "./controllers/authentication.controller"; +import { secureClientRoutesWithJWTs } from "./utils/auth"; const app = express(); const server = http.createServer(app); @@ -23,9 +25,15 @@ 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("Hello World"); }); +// AUTHENTICATION ENDPOINTS +app.get("/get-server-api-key", getServerAPIKey); +app.get("/get-chat-token",getChatToken); // USER ENDPOINTS app.post("/users/create", createUser); diff --git a/src/supabase.types.ts b/src/supabase.types.ts index fe24fbd..218ab3f 100644 --- a/src/supabase.types.ts +++ b/src/supabase.types.ts @@ -74,16 +74,19 @@ export interface Database { created_at: string id: string username: string + apikey?: string } Insert: { created_at: string id?: string username: string + apikey?: string } Update: { created_at?: string id?: string username?: string + apikey?: string } } } diff --git a/src/types.ts b/src/types.ts index e09244e..1b63e81 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 @@ -58,4 +64,11 @@ export interface SocketConnectedUsers { export interface SocketSocketIdUserId { [key: string]: string -} \ No newline at end of file +} + +export interface UserPayLoad { + username:string; + team: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..eda2101 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,84 @@ +import { NextFunction, Request, Response } from "express"; +import jwt from "jsonwebtoken" +import crypto from 'crypto' + +import { UserPayLoad } from "../types"; +import supabase from "../utils/supabase" + +async 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 {team,username} = await jwt.verify(token,jwtKey) as UserPayLoad + if (!team){ + return false + } + if (!username){ + return false + } + return true + }catch(err){ + return false + } + + } + +export const secureClientRoutesWithJWTs = async (req:Request, res:Response, next:NextFunction) =>{ + const nonSecureRoutes = ['/get-chat-token','/get-server-api-key'] + 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 = await verifyToken(jwt) + + if (!isVerified){ + return res.status(401).json({ error: 'Invalid token' }); + } + + next(); + } + + + +export const newAPIKey = async function (username:string) { + const newKey = crypto.randomUUID(); + try{ + const {data, error } = await supabase + .from('users') + .update({ apikey: newKey }) + .eq('username', username) + if (error){ + console.log(error) + } + else{ + console.log(data) + return newKey + } + }catch(err){ + console.log(err) + } +} +export const isValidAPIKey = async function (username:string, apikey:string) { + + try{ + const { data, error } = await supabase + .from('users') + .select('apikey') + .eq('username', username) + if (error){ + + return false + } + else{ + if (data.length === 0) return false + const storedAPIKey = data[0].apikey + return apikey === storedAPIKey + } + }catch(err){ + return false + } +} \ No newline at end of file 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== From a4c2364682265e5f3f0c1784fc8efdca29f8e2ff Mon Sep 17 00:00:00 2001 From: Jack Bridger Date: Mon, 13 Feb 2023 10:23:06 +0000 Subject: [PATCH 2/5] add the routes for new api --- src/index.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index d937b60..be23605 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,10 +8,10 @@ import { createUser, searchUsers } from './controllers/user.controller'; import Socket from "./utils/socket"; import { - createConversation, - addMessageToConversation, - getAllConversations, - getConversationMessages + createConversation as createChannel, + addMessageToConversation as addMessageToAChannel, + getAllConversations as getAllChannels, + getConversationMessages as getMessagesInAChannel } from './controllers/conversation.controller'; import { getServerAPIKey,getChatToken } from "./controllers/authentication.controller"; import { secureClientRoutesWithJWTs } from "./utils/auth"; @@ -37,14 +37,31 @@ app.get("/get-chat-token",getChatToken); // USER ENDPOINTS app.post("/users/create", createUser); -app.get("/users/search", searchUsers); +app.get("/users", () => console.log("get all users")); +app.post("/users", () => console.log("create new user with given username")); +app.get("/users/:user_id", () => console.log("Get data of a given user")); +app.put("users/:user_id", () => console.log("Update user data")); +app.delete("users/:user_id", () => console.log("Delete user")); // CONVERSATION ENDPOINTS -app.post("/conversations/create", createConversation); -app.get("/conversations", getAllConversations) -app.get("/conversations/:conversation_id/messages", getConversationMessages) +app.post("/channels", createChannel); +app.get("channels/:channel_id", () => console.log("get channel data")); +app.put("channels/:channel_id", () => console.log("update channel data")); +app.delete("channels/:channel_id", () => console.log("delete channel")); +app.get("/channels", getAllChannels) +app.get("/channels/:channel_id/messages", getMessagesInAChannel) + +app.post("/channels/:channel_id/users/:user_id", () => console.log("join a channel")); // SEND A MESSAGE -app.post("/conversations/:conversation_id/messages/create", addMessageToConversation) +app.post("/messages", addMessageToAChannel) +app.get("messages/:message_id", () => console.log("get message data")); +app.put("messages/:message_id", () => console.log("update message data")); +app.delete("messages/:message_id", () => console.log("delete message")); + +// Unclear parts +app.post("users/connect", () => console.log("connect user to all channels")); +app.get("/users/search?q=:query", searchUsers); + server.listen(3000); \ No newline at end of file From 53d226fb0d2eb0413a95158322b7343fa59b8eb1 Mon Sep 17 00:00:00 2001 From: Jack Bridger Date: Mon, 13 Feb 2023 18:32:59 +0000 Subject: [PATCH 3/5] auth working pretty well --- commands.sql | 30 +++ src/controllers/authentication.controller.ts | 56 +++-- src/controllers/channel.controller.ts | 221 +++++++++++++++++++ src/controllers/conversation.controller.ts | 175 --------------- src/controllers/message.controller.ts | 92 ++++++++ src/controllers/user.controller.ts | 68 +++++- src/index.ts | 50 +++-- src/supabase.types.ts | 16 +- src/types.ts | 7 +- src/utils/auth.ts | 46 ++-- src/utils/socket.ts | 6 +- 11 files changed, 521 insertions(+), 246 deletions(-) create mode 100644 commands.sql create mode 100644 src/controllers/channel.controller.ts delete mode 100644 src/controllers/conversation.controller.ts create mode 100644 src/controllers/message.controller.ts diff --git a/commands.sql b/commands.sql new file mode 100644 index 0000000..3bba958 --- /dev/null +++ b/commands.sql @@ -0,0 +1,30 @@ +CREATE TABLE companies ( + id UUID PRIMARY KEY default uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + owner_user_id UUID REFERENCES users(id) NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE TABLE apps ( + id UUID PRIMARY KEY default uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + owner_user_id UUID REFERENCES users(id) NOT NULL, + company_id UUID references companies(id) NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE TABLE api_keys ( + id UUID PRIMARY KEY default uuid_generate_v4(), + name VARCHAR(255) NOT NULL, + key_hash VARCHAR(255) NOT NULL, + owner_user_id UUID REFERENCES users(id) NOT NULL, + company_id UUID references companies(id) NOT NULL, + app_id UUID references apps(id) NOT NULL, + created_at TIMESTAMP NOT NULL +); + + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON apps, api_keys, companies, channels,messages,users +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); \ No newline at end of file diff --git a/src/controllers/authentication.controller.ts b/src/controllers/authentication.controller.ts index 6ed72eb..a532dd0 100644 --- a/src/controllers/authentication.controller.ts +++ b/src/controllers/authentication.controller.ts @@ -5,17 +5,28 @@ import { } from '../types'; import { newAPIKey,isValidAPIKey } from "../utils/auth"; - - - -export const getServerAPIKey = async (req:TypedRequestBody<{username:string}>, res:Response) => { - const username = req.body.username - if (!username){ - return res.status(400).json({ error: 'No username sent!' }); +export const getServerAPIKey = async (req:TypedRequestBody<{appID:string}>, res:Response) => { + // Get that from Supabase Auth + const userID = "3145fe5e-3ddd-4604-8765-76939de12dd5" + // CompanyID should come from the users auth id? + const companyID = "d17f074e-baa9-4391-b24e-0c41f7944553" + const appID = req.body.appID + console.log({appID}) + if (!userID){ + 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(username) + const apiKey = await newAPIKey({userID,appID,companyID}) + if (!apiKey){ + return res.status(400).json({ error: 'Could not generate API key!' }); + } return res.send(apiKey) } @@ -25,21 +36,36 @@ export const getServerAPIKey = async (req:TypedRequestBody<{username:string}>, r return res.status(403).json({ error: 'No server key sent!' }); } if (!jwtKey) throw new Error("No JWT key found") - const username = req.body.username - if (!username){ + // 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 + + // TO DO - We figure out team from app ID + const companyID = "123" + 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(username,apikey) + const isValid = await isValidAPIKey(appID,apikey) if (!isValid){ return res.status(400).json({ error: 'Invalid API key' }); } - const team = "tinder"; - const claims = { team,username } - const token = await jwt.sign(claims, jwtKey,{ - expiresIn:60 + const claims = { userID,companyID,appID } + // To do - make async? + const token = jwt.sign(claims, jwtKey,{ + expiresIn:600 // 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/channel.controller.ts b/src/controllers/channel.controller.ts new file mode 100644 index 0000000..0c7c81d --- /dev/null +++ b/src/controllers/channel.controller.ts @@ -0,0 +1,221 @@ +import { Response } from "express" +import supabase from "../utils/supabase" +import Socket from '../utils/socket'; +import { + TypedRequestBody, + TypedRequestQuery, + TypedRequestQueryAndParams, + User, + Channel +} from '../types'; + +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<{owner_id: string, participant_ids: string[], group_name: string}>, res: Response) { + const { + owner_id, + participant_ids, + group_name, + } = req.body; + + // first create the channel + const channel = await supabase + .from('channels') + .upsert({ + name: group_name, + owner_user_id: owner_id, + created_at: ((new Date()).toISOString()).toLocaleString() + }) + .select() + + if (channel.error) { + res.send(500) + } + + let participants: User[] = []; + + if (participant_ids.length > 1 && channel.data?.length) { + // attach all our users to this channel + const pivotData = await supabase + .from('user_channel') + .upsert(participant_ids.map((participant_id) => { + return { + user_id: participant_id, + channel_id: channel.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 (channel.error) { + return res.sendStatus(500) + } else { + const conv: Channel = { + ...channel.data[0], + participants + }; + + Socket.notifyUsersOnChannelCreate(participant_ids as string[], conv) + return res.send(conv); + } +} + +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) +} + +export const getChannelByID = async function (req: TypedRequestQueryAndParams<{channel_id: string} ,{last_message_date: Date}>, res: Response) { + // TODO - write this code + 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) +} + +export const updateChannelByID = async function (req: TypedRequestQueryAndParams<{channel_id: string} ,{last_message_date: Date}>, res: Response) { + // TODO - write this code + + 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) +} + +export const deleteChannelByID = async function (req: TypedRequestQueryAndParams<{channel_id: string} ,{last_message_date: Date}>, res: Response) { + // TODO - write this code + + 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) +} \ 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/message.controller.ts b/src/controllers/message.controller.ts new file mode 100644 index 0000000..5c13ba7 --- /dev/null +++ b/src/controllers/message.controller.ts @@ -0,0 +1,92 @@ +import { Response } from "express" +import supabase from "../utils/supabase" +import Socket from '../utils/socket'; +import { + TypedRequestQueryWithBodyAndParams, + Message, + TypedRequestBody +} from '../types'; + + +export const sendMessageToChannel = async function (req: TypedRequestQueryWithBodyAndParams<{channel_id: string}, {user_id: string, message: string}>, res: Response) { + const channelID = req.params.channel_id; + const { + user_id, + message, + } = req.body; + + const data = await supabase + .from('messages') + .upsert({ + channel_id: channelID, + user_id, + message, + created_at: ((new Date()).toISOString()).toLocaleString() + }) + .select(` + *, + users ( + id, + username + ), + channels (*) + `) + + // get the users in this chat, except for the current one + const userChannelIds = await supabase + .from('user_channel') + .select('user_id') + .eq('channel', channelID) + + 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] + ) + } +} + +export const getMessageByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { + // TODO - write this code + const { data, error } = await supabase + .from('users') + .select() + + if (error) { + res.send(500) + } else { + res.send(data[0]) + } +} + +export const updateMessageByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { + // TODO - write this code + const { data, error } = await supabase + .from('users') + .select() + + if (error) { + res.send(500) + } else { + res.send(data[0]) + } +} + +export const deleteMessageByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { + // TODO - write this code + const { data, error } = await supabase + .from('users') + .select() + + if (error) { + res.send(500) + } else { + res.send(data[0]) + } +} \ No newline at end of file diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index a84f8d9..43593f7 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -17,9 +17,58 @@ export const createUser = async function (req: TypedRequestBody<{username: strin res.send(data[0]) } } +export const getAllUsers = async function (req: TypedRequestBody<{username: string}>, res: Response) { + // TODO - write this code + const { data, error } = await supabase + .from('users') + .select() + + if (error) { + res.send(500) + } else { + res.send(data[0]) + } +} +export const getUserByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { + // TODO - write this code + const { data, error } = await supabase + .from('users') + .select() + + if (error) { + res.send(500) + } else { + res.send(data[0]) + } +} +export const updateUserByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { + // TODO - write this code + const { data, error } = await supabase + .from('users') + .select() + + if (error) { + res.send(500) + } else { + res.send(data[0]) + } +} +export const deleteUserByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { + // TODO - write this code + const { data, error } = await supabase + .from('users') + .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) { - + // To do - make sure matches what I put in api let query = supabase .from('users') .select(); @@ -38,4 +87,21 @@ export const searchUsers = async function (req: TypedRequestQuery<{user_id: st } else { 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) { + res.send(500) + } else { + res.send(data[0]) + } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index be23605..e84c83b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,15 +4,18 @@ 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, updateUserByID, deleteUserByID, connectUser } from './controllers/user.controller'; import Socket from "./utils/socket"; import { - createConversation as createChannel, - addMessageToConversation as addMessageToAChannel, - getAllConversations as getAllChannels, - getConversationMessages as getMessagesInAChannel -} from './controllers/conversation.controller'; + createChannel as createChannel, + deleteChannelByID, + getAllChannels as getAllChannels, + getChannelByID, + getChannelMessages as getMessagesInAChannel, + updateChannelByID +} from './controllers/channel.controller'; +import { deleteMessageByID, getMessageByID, sendMessageToChannel, updateMessageByID } from "./controllers/message.controller"; import { getServerAPIKey,getChatToken } from "./controllers/authentication.controller"; import { secureClientRoutesWithJWTs } from "./utils/auth"; @@ -29,38 +32,37 @@ app.use(cors()) app.use(secureClientRoutesWithJWTs); app.get("/", function (req, res) { - return res.send("Hello World"); + return res.send("Thanks for using our chat API. Please check out our docs"); }); // AUTHENTICATION ENDPOINTS app.get("/get-server-api-key", getServerAPIKey); app.get("/get-chat-token",getChatToken); - + // USER ENDPOINTS -app.post("/users/create", createUser); -app.get("/users", () => console.log("get all users")); -app.post("/users", () => console.log("create new user with given username")); -app.get("/users/:user_id", () => console.log("Get data of a given user")); -app.put("users/:user_id", () => console.log("Update user data")); -app.delete("users/:user_id", () => console.log("Delete user")); +app.get("/users", getAllUsers); +app.post("/users", createUser); +app.get("/users/:user_id", getUserByID); +app.put("users/:user_id", updateUserByID ); +app.delete("users/:user_id", deleteUserByID); -// CONVERSATION ENDPOINTS +// CHANNEL ENDPOINTS app.post("/channels", createChannel); -app.get("channels/:channel_id", () => console.log("get channel data")); -app.put("channels/:channel_id", () => console.log("update channel data")); -app.delete("channels/:channel_id", () => console.log("delete channel")); +app.get("channels/:channel_id", getChannelByID); +app.put("channels/:channel_id", updateChannelByID); +app.delete("channels/:channel_id", deleteChannelByID); app.get("/channels", getAllChannels) app.get("/channels/:channel_id/messages", getMessagesInAChannel) app.post("/channels/:channel_id/users/:user_id", () => console.log("join a channel")); -// SEND A MESSAGE -app.post("/messages", addMessageToAChannel) -app.get("messages/:message_id", () => console.log("get message data")); -app.put("messages/:message_id", () => console.log("update message data")); -app.delete("messages/:message_id", () => console.log("delete message")); +// Messages +app.post("/messages", sendMessageToChannel) +app.get("messages/:message_id",getMessageByID); +app.put("messages/:message_id", updateMessageByID); +app.delete("messages/:message_id", deleteMessageByID); // Unclear parts -app.post("users/connect", () => console.log("connect user to all channels")); +app.post("users/connect", connectUser); app.get("/users/search?q=:query", searchUsers); diff --git a/src/supabase.types.ts b/src/supabase.types.ts index 218ab3f..73eae49 100644 --- a/src/supabase.types.ts +++ b/src/supabase.types.ts @@ -9,7 +9,7 @@ export type Json = export interface Database { public: { Tables: { - conversations: { + channels: { Row: { created_at: string id: string @@ -31,40 +31,40 @@ export interface Database { } messages: { Row: { - conversation_id: string | null + channel_id: string | null created_at: string id: string message: string user_id: string | null } Insert: { - conversation_id?: string | null + channel_id?: string | null created_at: string id?: string message: string user_id?: string | null } Update: { - conversation_id?: string | null + channel_id?: string | null created_at?: string id?: string message?: 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 } diff --git a/src/types.ts b/src/types.ts index 1b63e81..6436213 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,7 +34,7 @@ export interface User { created_at: string; } -export interface Conversation { +export interface Channel { id: string; name: string; owner_user_id: string; @@ -49,9 +49,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 { @@ -69,6 +69,7 @@ export interface SocketSocketIdUserId { export interface UserPayLoad { username:string; team: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 index eda2101..6310035 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -10,13 +10,16 @@ async function verifyToken(token:string) { if (!jwtKey) throw new Error("No JWT key found") if (!token) return false try{ - const {team,username} = await jwt.verify(token,jwtKey) as UserPayLoad + const {team,username,appID} = await jwt.verify(token,jwtKey) as UserPayLoad if (!team){ return false } if (!username){ return false } + if (!appID){ + return false + } return true }catch(err){ return false @@ -41,16 +44,16 @@ export const secureClientRoutesWithJWTs = async (req:Request, res:Response, next next(); } - - -export const newAPIKey = async function (username:string) { +export const newAPIKey = async function (keyDetails:{userID:string,appID:string,companyID:string}) { + const {userID, appID,companyID} = keyDetails const newKey = crypto.randomUUID(); + try{ const {data, error } = await supabase - .from('users') - .update({ apikey: newKey }) - .eq('username', username) + .from('api_keys') + .upsert({ key: newKey,app_id:appID,company_id:companyID,owner_user_id:userID,name:"new API key"}) + .eq('app_id', appID) if (error){ console.log(error) } @@ -62,21 +65,30 @@ export const newAPIKey = async function (username:string) { console.log(err) } } -export const isValidAPIKey = async function (username:string, apikey:string) { - - try{ +export const isValidAPIKey = async function (appID:string, receivedAPIKey:string) { + try{ + console.log({appID}) + console.log({receivedAPIKey}) const { data, error } = await supabase - .from('users') - .select('apikey') - .eq('username', username) + .from('api_keys') + .select('key') + .eq('app_id', appID) + .eq('key', receivedAPIKey) if (error){ - + console.log(error) return false } else{ - if (data.length === 0) return false - const storedAPIKey = data[0].apikey - return apikey === storedAPIKey + if (data.length === 0) { + console.log("No api key found") + return false + } + if (data.length > 1) { + console.log("MORE THAN ONE API KEY FOUND") + return false + } + const storedAPIKey = data[0].key + return receivedAPIKey === storedAPIKey } }catch(err){ return false 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); } }) } From f7b3937834de12da399144a26ade2b3a0a783463 Mon Sep 17 00:00:00 2001 From: Jack Bridger Date: Mon, 13 Feb 2023 22:24:12 +0000 Subject: [PATCH 4/5] slowly getting htere --- src/controllers/apps.controller.ts | 55 +++++++++ ...l.controller.ts => channels.controller.ts} | 0 src/controllers/companies.controller.ts | 31 +++++ ...e.controller.ts => messages.controller.ts} | 0 ...user.controller.ts => users.controller.ts} | 35 +++--- src/index.ts | 15 ++- src/supabase.types.ts | 106 +++++++++++++++++- src/types.ts | 4 +- src/utils/auth.ts | 15 ++- 9 files changed, 230 insertions(+), 31 deletions(-) create mode 100644 src/controllers/apps.controller.ts rename src/controllers/{channel.controller.ts => channels.controller.ts} (100%) create mode 100644 src/controllers/companies.controller.ts rename src/controllers/{message.controller.ts => messages.controller.ts} (100%) rename src/controllers/{user.controller.ts => users.controller.ts} (76%) diff --git a/src/controllers/apps.controller.ts b/src/controllers/apps.controller.ts new file mode 100644 index 0000000..6222590 --- /dev/null +++ b/src/controllers/apps.controller.ts @@ -0,0 +1,55 @@ +import { Response } from "express" +import { + TypedRequestBody, TypedRequestQueryWithParams, +} from '../types'; +import supabase from "../utils/supabase" + +export const createNewApp = async (req:TypedRequestBody<{name:string,user_id:string}>, res:Response) => { + const name = req.body.name + const userID = req.body.user_id + // TODO - need to get companyID myself from user. + const companyID = "d17f074e-baa9-4391-b24e-0c41f7944553" + // create new app in supabase + try { + const { data, error } = await supabase + .from('apps') + .upsert({ + name: name, + owner_user_id: userID, + company_id: companyID + }) + .select() + if (error) { + console.log(error) + return res.status(500).json({error:"Could not create app"}) + } else { + console.log(data) + res.send(data[0]) + } + }catch(err) { + console.log(err) + return res.status(500).json({error:"Could not create app"}) + } + } + +export const deleteAppByID = async (req:TypedRequestQueryWithParams<{app_id:string}>, res:Response) => { + const appID = req.params.app_id + try { + const { error } = await supabase + .from('apps') + .delete() + .eq('id', appID) + if (error) { + console.log(error) + return res.status(500).json({error:"Could not delete app"}) + } else { + res.send("app deleted") + } + }catch(err) { + console.log(err) + return res.status(500).json({error:"Could not delete app"}) + } + } + + + diff --git a/src/controllers/channel.controller.ts b/src/controllers/channels.controller.ts similarity index 100% rename from src/controllers/channel.controller.ts rename to src/controllers/channels.controller.ts diff --git a/src/controllers/companies.controller.ts b/src/controllers/companies.controller.ts new file mode 100644 index 0000000..dd4dd6f --- /dev/null +++ b/src/controllers/companies.controller.ts @@ -0,0 +1,31 @@ +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/message.controller.ts b/src/controllers/messages.controller.ts similarity index 100% rename from src/controllers/message.controller.ts rename to src/controllers/messages.controller.ts diff --git a/src/controllers/user.controller.ts b/src/controllers/users.controller.ts similarity index 76% rename from src/controllers/user.controller.ts rename to src/controllers/users.controller.ts index 43593f7..20ec45f 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/users.controller.ts @@ -1,6 +1,6 @@ import { Response } from "express" import supabase from "../utils/supabase" -import { TypedRequestBody, TypedRequestQuery } from "../types" +import { TypedRequestBody, TypedRequestQuery, TypedRequestQueryWithParams } from "../types" export const createUser = async function (req: TypedRequestBody<{username: string}>, res: Response) { const { data, error } = await supabase @@ -10,18 +10,19 @@ export const createUser = async function (req: TypedRequestBody<{username: strin created_at: ((new Date()).toISOString()).toLocaleString() }) .select() - if (error) { res.send(500) } else { res.send(data[0]) } } -export const getAllUsers = async function (req: TypedRequestBody<{username: string}>, res: Response) { - // TODO - write this code +export const getAllUsers = async function (req: TypedRequestBody<{app_id: string;}>, res: Response) { + // TO DO add pagination + const appID = req.body.app_id const { data, error } = await supabase - .from('users') - .select() + .from('users') + .select() + .eq('app_id', appID) if (error) { res.send(500) @@ -29,11 +30,13 @@ export const getAllUsers = async function (req: TypedRequestBody<{username: stri res.send(data[0]) } } -export const getUserByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { - // TODO - write this code +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() + .from('users') + .select() + .eq('id', userID) if (error) { res.send(500) @@ -41,11 +44,17 @@ export const getUserByID = async function (req: TypedRequestBody<{username: stri res.send(data[0]) } } -export const updateUserByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { - // TODO - write this code +export const updateUserByID = async function (req: TypedRequestBody<{new_username: string}>, res: Response) { + // TO DO - get userID from jwt + // And test this one + const userID = "" + const newUsername = req.body.new_username + const { data, error } = await supabase .from('users') - .select() + .update({ + username: newUsername + }).eq('id', userID).select() if (error) { res.send(500) diff --git a/src/index.ts b/src/index.ts index e84c83b..24e562d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import cors from 'cors'; import http from 'http'; import { Server } from 'socket.io'; -import { createUser, searchUsers, getAllUsers, getUserByID, updateUserByID, deleteUserByID, connectUser } from './controllers/user.controller'; +import { createUser, searchUsers, getAllUsers, getUserByID, updateUserByID, deleteUserByID, connectUser } from './controllers/users.controller'; import Socket from "./utils/socket"; import { @@ -14,10 +14,12 @@ import { getChannelByID, getChannelMessages as getMessagesInAChannel, updateChannelByID -} from './controllers/channel.controller'; -import { deleteMessageByID, getMessageByID, sendMessageToChannel, updateMessageByID } from "./controllers/message.controller"; +} 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"; const app = express(); const server = http.createServer(app); @@ -61,6 +63,13 @@ app.get("messages/:message_id",getMessageByID); app.put("messages/:message_id", updateMessageByID); app.delete("messages/:message_id", deleteMessageByID); +// App +app.post("/apps",createNewApp) +app.delete("/apps/:app_id", deleteAppByID); + +// Companies +app.post("/companies", createNewCompany); + // Unclear parts app.post("users/connect", connectUser); app.get("/users/search?q=:query", searchUsers); diff --git a/src/supabase.types.ts b/src/supabase.types.ts index 73eae49..8d5da1b 100644 --- a/src/supabase.types.ts +++ b/src/supabase.types.ts @@ -9,24 +9,111 @@ export type Json = export interface Database { public: { Tables: { + api_keys: { + Row: { + app_id: string + company_id: string + created_at: string + id: string + key: string + name: string + owner_user_id: string + updated_at: string | null + } + Insert: { + app_id: string + company_id: string + created_at?: string + id?: string + key: string + name: string + owner_user_id: string + updated_at?: string | null + } + Update: { + app_id?: string + company_id?: string + created_at?: string + id?: string + key?: string + name?: string + owner_user_id?: string + updated_at?: string | null + } + } + apps: { + Row: { + company_id: string + created_at: string + id: string + name: string + owner_user_id: string + updated_at: string | null + } + Insert: { + company_id: string + created_at?: string + id?: string + name: string + owner_user_id: string + updated_at?: string | null + } + Update: { + company_id?: string + created_at?: string + id?: string + name?: string + owner_user_id?: string + updated_at?: string | null + } + } channels: { Row: { + app_id: string created_at: string id: string name: string owner_user_id: string + updated_at: string | null } Insert: { + app_id: string + created_at?: string + id?: string + name: string + owner_user_id: string + updated_at?: string | null + } + Update: { + app_id?: string + created_at?: string + id?: string + name?: string + owner_user_id?: string + updated_at?: string | null + } + } + companies: { + Row: { created_at: string + id: string + name: string + owner_user_id: string + updated_at: string | null + } + Insert: { + created_at?: string id?: string name: string owner_user_id: string + updated_at?: string | null } Update: { created_at?: string id?: string name?: string owner_user_id?: string + updated_at?: string | null } } messages: { @@ -35,13 +122,15 @@ export interface Database { created_at: string id: string message: string + updated_at: string | null user_id: string | null } Insert: { channel_id?: string | null - created_at: string + created_at?: string id?: string message: string + updated_at?: string | null user_id?: string | null } Update: { @@ -49,6 +138,7 @@ export interface Database { created_at?: string id?: string message?: string + updated_at?: string | null user_id?: string | null } } @@ -71,22 +161,25 @@ export interface Database { } users: { Row: { + app_id: string | null created_at: string id: string + updated_at: string | null username: string - apikey?: string } Insert: { - created_at: string + app_id?: string | null + created_at?: string id?: string + updated_at?: string | null username: string - apikey?: string } Update: { + app_id?: string | null created_at?: string id?: string + updated_at?: string | null username?: string - apikey?: string } } } @@ -99,5 +192,8 @@ export interface Database { Enums: { [_ in never]: never } + CompositeTypes: { + [_ in never]: never + } } } diff --git a/src/types.ts b/src/types.ts index 6436213..9ea955f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,8 +67,8 @@ export interface SocketSocketIdUserId { } export interface UserPayLoad { - username:string; - team:string; + userID:string; + companyID:string; appID:string; iat:number; exp:number diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 6310035..40310be 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -5,16 +5,16 @@ import crypto from 'crypto' import { UserPayLoad } from "../types"; import supabase from "../utils/supabase" -async function verifyToken(token:string) { +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 {team,username,appID} = await jwt.verify(token,jwtKey) as UserPayLoad - if (!team){ + const {userID,companyID,appID} = jwt.verify(token,jwtKey) as UserPayLoad + if (!companyID){ return false } - if (!username){ + if (!userID){ return false } if (!appID){ @@ -28,7 +28,8 @@ async function verifyToken(token:string) { } export const secureClientRoutesWithJWTs = async (req:Request, res:Response, next:NextFunction) =>{ - const nonSecureRoutes = ['/get-chat-token','/get-server-api-key'] + // TO DO - need to make sure apps and companies are secure + const nonSecureRoutes = ['/get-chat-token','/get-server-api-key',"/apps","/companies"] if (nonSecureRoutes.includes(req.path)){ return next() } @@ -36,7 +37,7 @@ export const secureClientRoutesWithJWTs = async (req:Request, res:Response, next return res.status(403).json({ error: 'No credentials sent!' }); } const jwt = req.headers.authorization.split(' ')[1] - const isVerified = await verifyToken(jwt) + const isVerified = verifyToken(jwt) if (!isVerified){ return res.status(401).json({ error: 'Invalid token' }); @@ -67,8 +68,6 @@ export const newAPIKey = async function (keyDetails:{userID:string,appID:string, } export const isValidAPIKey = async function (appID:string, receivedAPIKey:string) { try{ - console.log({appID}) - console.log({receivedAPIKey}) const { data, error } = await supabase .from('api_keys') .select('key') From 69955dede7928b4f07d341da25f00e8369bdfcc6 Mon Sep 17 00:00:00 2001 From: Jack Bridger Date: Wed, 15 Feb 2023 22:07:12 +0000 Subject: [PATCH 5/5] get a lot of the routes working --- .eslintrc.js | 1 + commands.sql | 157 ++++++++- src/controllers/apps.controller.ts | 155 +++++++-- src/controllers/authentication.controller.ts | 46 ++- src/controllers/channels.controller.ts | 321 ++++++++++++------- src/controllers/companies.controller.ts | 3 +- src/controllers/developers.controller.ts | 70 ++++ src/controllers/messages.controller.ts | 230 ++++++++++--- src/controllers/users.controller.ts | 176 +++++++--- src/index.ts | 53 +-- src/supabase.types.ts | 249 +++++++++++--- src/types.ts | 3 +- src/utils/auth.ts | 133 ++++++-- 13 files changed, 1242 insertions(+), 355 deletions(-) create mode 100644 src/controllers/developers.controller.ts 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 index 3bba958..9872ae3 100644 --- a/commands.sql +++ b/commands.sql @@ -1,30 +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, - owner_user_id UUID REFERENCES users(id) NOT NULL, - created_at TIMESTAMP 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, - owner_user_id UUID REFERENCES users(id) NOT NULL, - company_id UUID references companies(id) NOT NULL, - created_at TIMESTAMP 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, - key_hash VARCHAR(255) NOT NULL, - owner_user_id UUID REFERENCES users(id) NOT NULL, - company_id UUID references companies(id) NOT NULL, - app_id UUID references apps(id) NOT NULL, - created_at TIMESTAMP 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 apps, api_keys, companies, channels,messages,users +BEFORE UPDATE ON messages FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); \ No newline at end of file diff --git a/src/controllers/apps.controller.ts b/src/controllers/apps.controller.ts index 6222590..69043f1 100644 --- a/src/controllers/apps.controller.ts +++ b/src/controllers/apps.controller.ts @@ -4,52 +4,149 @@ import { } from '../types'; import supabase from "../utils/supabase" -export const createNewApp = async (req:TypedRequestBody<{name:string,user_id:string}>, res:Response) => { +export const createNewApp = async (req:TypedRequestBody<{name:string,developer_id:string}>, res:Response) => { const name = req.body.name - const userID = req.body.user_id + const devID = req.body.developer_id // TODO - need to get companyID myself from user. - const companyID = "d17f074e-baa9-4391-b24e-0c41f7944553" - // create new app in supabase - try { + 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, - owner_user_id: userID, - company_id: companyID - }) + name:name + }) .select() if (error) { console.log(error) - return res.status(500).json({error:"Could not create app"}) + return null; } else { - console.log(data) - res.send(data[0]) + return data[0].id } - }catch(err) { + } + catch(err){ console.log(err) - return res.status(500).json({error:"Could not create app"}) + return null } - } - -export const deleteAppByID = async (req:TypedRequestQueryWithParams<{app_id:string}>, res:Response) => { - const appID = req.params.app_id - try { - const { error } = await supabase - .from('apps') - .delete() - .eq('id', appID) +} +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 res.status(500).json({error:"Could not delete app"}) + return null; } else { - res.send("app deleted") + return data[0].id } - }catch(err) { + }catch(err){ console.log(err) - return res.status(500).json({error:"Could not delete app"}) - } - } + 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 index a532dd0..7895598 100644 --- a/src/controllers/authentication.controller.ts +++ b/src/controllers/authentication.controller.ts @@ -4,15 +4,16 @@ 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 userID = "3145fe5e-3ddd-4604-8765-76939de12dd5" + 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 - console.log({appID}) - if (!userID){ + + if (!devID){ return res.status(400).json({ error: 'No username!' }); } if (!appID){ @@ -23,13 +24,40 @@ export const getServerAPIKey = async (req:TypedRequestBody<{appID:string}>, res: } //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,appID,companyID}) + 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) { @@ -39,12 +67,12 @@ export const getServerAPIKey = async (req:TypedRequestBody<{appID:string}>, res: // 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 - - // TO DO - We figure out team from app ID - const companyID = "123" + + const companyID = await getCompanyIDFromAppID(appID) + console.log(companyID) + if (!userID){ return res.status(403).json({error:"No username"}) } @@ -65,7 +93,7 @@ export const getServerAPIKey = async (req:TypedRequestBody<{appID:string}>, res: const claims = { userID,companyID,appID } // To do - make async? const token = jwt.sign(claims, jwtKey,{ - expiresIn:600 // TO DO - need to make this longer & revokable and renewable + 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 index 0c7c81d..5fbbb81 100644 --- a/src/controllers/channels.controller.ts +++ b/src/controllers/channels.controller.ts @@ -1,13 +1,13 @@ -import { Response } from "express" +import { Response, Request } from "express" import supabase from "../utils/supabase" import Socket from '../utils/socket'; import { TypedRequestBody, TypedRequestQuery, TypedRequestQueryAndParams, - User, - Channel + 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 @@ -40,65 +40,102 @@ export const getAllChannels = async function (req: TypedRequestQuery<{user_id: s return res.send(channels.data) } -export const createChannel = async function (req: TypedRequestBody<{owner_id: string, participant_ids: string[], group_name: string}>, res: Response) { +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 { - owner_id, 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: group_name, - owner_user_id: owner_id, - created_at: ((new Date()).toISOString()).toLocaleString() - }) - .select() + .from('channels') + .upsert({ + name: name, + owner_user_id:userID + }) + .select() if (channel.error) { - res.send(500) + 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() - let participants: User[] = []; + if (channel.error) { + return null + } + else return channel.data +} +const addParticipantsToChannel = async function(channelID:string, participantIDs:string[]) { - if (participant_ids.length > 1 && channel.data?.length) { - // attach all our users to this channel - const pivotData = await supabase + try{ + await participantIDs.map(async paricipantID => { + console.log({paricipantID}) + const {data,error} = await supabase .from('user_channel') - .upsert(participant_ids.map((participant_id) => { - return { - user_id: participant_id, - channel_id: channel.data[0].id - } - })) + .upsert({ + user_id: paricipantID, + channel_id: channelID + }) .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 (error) { + console.log(error) + return false } - } - - if (channel.error) { - return res.sendStatus(500) - } else { - const conv: Channel = { - ...channel.data[0], - participants - }; + if (data){ + return true + } + }) - Socket.notifyUsersOnChannelCreate(participant_ids as string[], conv) - return res.send(conv); + }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; @@ -127,95 +164,151 @@ export const getChannelMessages = async function (req: TypedRequestQueryAndParam 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; - - let query = supabase - .from('messages') + try { + const {error,data} = await supabase + .from('channels') .select(` - id, - channel_id, - message, - created_at, - - users ( + name, + my_messages:message_channel( id, - username + messages:messages( + id, + message, + who_sent:message_user( + user_id:users( + username + ) + ) + ) ) `) - .order('created_at', { ascending: true }) - .eq('channel_id', channel_id) - - if (last_message_date){ - query = query.gt('created_at', last_message_date) + // .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) } - const messages = await query; + // res.send(messages.data) + }catch(err){ + console.log(err) + res.sendStatus(500) + } - res.send(messages.data) + } -export const updateChannelByID = async function (req: TypedRequestQueryAndParams<{channel_id: string} ,{last_message_date: Date}>, res: Response) { - // TODO - write this code - +export const updateChannelByID = async function (req: TypedRequestQueryWithBodyAndParams<{channel_id: string} ,{name:string;owner_user_id:string}>, res: Response) { const { channel_id } = req.params; - const { last_message_date } = req.query; + 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) + } - 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) -} export const deleteChannelByID = async function (req: TypedRequestQueryAndParams<{channel_id: string} ,{last_message_date: Date}>, res: Response) { - // TODO - write this code - - const { channel_id } = req.params; - const { last_message_date } = req.query; + 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) +} - 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 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 + } +} - const messages = await query; - res.send(messages.data) -} \ No newline at end of file + +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 index dd4dd6f..bd39dd3 100644 --- a/src/controllers/companies.controller.ts +++ b/src/controllers/companies.controller.ts @@ -28,4 +28,5 @@ export const createNewCompany = async (req:TypedRequestBody<{name:string,user_id console.log(err) return res.status(500).json({error:"Could not create company"}) } - } \ No newline at end of file + } + \ 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 index 5c13ba7..f48a403 100644 --- a/src/controllers/messages.controller.ts +++ b/src/controllers/messages.controller.ts @@ -2,91 +2,223 @@ import { Response } from "express" import supabase from "../utils/supabase" import Socket from '../utils/socket'; import { - TypedRequestQueryWithBodyAndParams, + TypedRequestQueryWithParams, Message, - TypedRequestBody + TypedRequestBody, + TypedRequestQuery, + TypedRequestQueryWithBodyAndParams } from '../types'; -export const sendMessageToChannel = async function (req: TypedRequestQueryWithBodyAndParams<{channel_id: string}, {user_id: string, message: string}>, res: Response) { - const channelID = req.params.channel_id; +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 data = await supabase + 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({ - channel_id: channelID, - user_id, message, - created_at: ((new Date()).toISOString()).toLocaleString() }) .select(` - *, - users ( - id, - username - ), - channels (*) + id `) - - // get the users in this chat, except for the current one - const userChannelIds = await supabase - .from('user_channel') - .select('user_id') - .eq('channel', channelID) - - 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) + 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 } - - res.send( - data.data[0] - ) + 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 -export const getMessageByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { - // TODO - write this code const { data, error } = await supabase - .from('users') - .select() + .from('messages') + .select(`*, sender:message_user(username:users(username))`) + .eq('id', messageID) if (error) { - res.send(500) + console.log(error) + res.sendStatus(500) } else { - res.send(data[0]) + 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: TypedRequestBody<{username: string}>, res: Response) { - // TODO - write this code +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('users') - .select() - + .from('messages') + .update({ + message: newMessage + }) + .eq('id', messageID) + .select() if (error) { - res.send(500) + console.log(error) + return res.sendStatus(500) } else { - res.send(data[0]) + return res.send(data[0]) } } -export const deleteMessageByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { +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('users') - .select() + .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) { - res.send(500) + console.log(error) + return null } else { - res.send(data[0]) + console.log("success") + console.log(data) + return data[0] } } \ No newline at end of file diff --git a/src/controllers/users.controller.ts b/src/controllers/users.controller.ts index 20ec45f..bfb5d4d 100644 --- a/src/controllers/users.controller.ts +++ b/src/controllers/users.controller.ts @@ -1,83 +1,183 @@ -import { Response } from "express" +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}>, res: Response) { +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: req.body.username, - created_at: ((new Date()).toISOString()).toLocaleString() + username: username, }) .select() if (error) { - res.send(500) + return null } else { - res.send(data[0]) + return data[0].id } } -export const getAllUsers = async function (req: TypedRequestBody<{app_id: string;}>, res: Response) { - // TO DO add pagination - const appID = req.body.app_id +const addUserToApp = async function(userID:string,appID:string){ const { data, error } = await supabase - .from('users') - .select() + .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) { - res.send(500) + return res.sendStatus(500) } else { - res.send(data[0]) + 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.send(500) + res.sendStatus(500) } else { res.send(data[0]) } } -export const updateUserByID = async function (req: TypedRequestBody<{new_username: string}>, res: Response) { - // TO DO - get userID from jwt - // And test this one - const userID = "" +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 - const { data, error } = await supabase + try { + const { data, error } = await supabase .from('users') .update({ username: newUsername - }).eq('id', userID).select() + }) + .eq('id', userID) + .select() if (error) { - res.send(500) + console.log(error) + return res.sendStatus(400) } else { - res.send(data[0]) + console.log(data) + + return res.sendStatus(200) + } + }catch(err){ + console.log(err) + return res.sendStatus(401) } + } -export const deleteUserByID = async function (req: TypedRequestBody<{username: string}>, res: Response) { - // TODO - write this code - const { data, error } = await supabase - .from('users') - .select() +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 - if (error) { - res.send(500) - } else { - res.send(data[0]) - } + 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(); @@ -92,9 +192,9 @@ export const searchUsers = async function (req: TypedRequestQuery<{user_id: st const { data, error } = await query; if (error) { - res.send(500) + return res.sendStatus(500) } else { - res.send(data) + return res.send(data) } } @@ -109,8 +209,8 @@ export const connectUser = async function (req: TypedRequestBody<{username: stri .select() if (error) { - res.send(500) + return res.sendStatus(500) } else { - res.send(data[0]) + return res.send(data[0]) } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 24e562d..9057230 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,13 +4,13 @@ import cors from 'cors'; import http from 'http'; import { Server } from 'socket.io'; -import { createUser, searchUsers, getAllUsers, getUserByID, updateUserByID, deleteUserByID, connectUser } from './controllers/users.controller'; +import { createUser, searchUsers, getAllUsers, getUserByID, updateCurrentUser, deleteUserByID, connectUser, deleteCurrentUser } from './controllers/users.controller'; import Socket from "./utils/socket"; import { createChannel as createChannel, deleteChannelByID, - getAllChannels as getAllChannels, + getAllChannels, getChannelByID, getChannelMessages as getMessagesInAChannel, updateChannelByID @@ -20,6 +20,7 @@ import { getServerAPIKey,getChatToken } from "./controllers/authentication.contr 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); @@ -36,42 +37,50 @@ app.use(secureClientRoutesWithJWTs); app.get("/", function (req, res) { 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); -app.get("/get-chat-token",getChatToken); +app.get("/get-server-api-key", getServerAPIKey); // done +app.get("/get-chat-token",getChatToken); // done (with some checks needed) // USER ENDPOINTS -app.get("/users", getAllUsers); -app.post("/users", createUser); -app.get("/users/:user_id", getUserByID); -app.put("users/:user_id", updateUserByID ); -app.delete("users/:user_id", deleteUserByID); +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); -app.get("channels/:channel_id", getChannelByID); -app.put("channels/:channel_id", updateChannelByID); -app.delete("channels/:channel_id", deleteChannelByID); -app.get("/channels", getAllChannels) +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) -app.get("messages/:message_id",getMessageByID); -app.put("messages/:message_id", updateMessageByID); -app.delete("messages/:message_id", deleteMessageByID); +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) -app.delete("/apps/:app_id", deleteAppByID); +app.post("/apps",createNewApp) // done +app.delete("/apps/:app_id", deleteAppByID); // need to do // Companies -app.post("/companies", createNewCompany); +app.post("/companies", createNewCompany); // dont need now // Unclear parts -app.post("users/connect", connectUser); +app.post("/users/connect", connectUser); app.get("/users/search?q=:query", searchUsers); diff --git a/src/supabase.types.ts b/src/supabase.types.ts index 8d5da1b..1120718 100644 --- a/src/supabase.types.ts +++ b/src/supabase.types.ts @@ -9,88 +9,121 @@ export type Json = export interface Database { public: { Tables: { + 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: { - app_id: string - company_id: string + api_key: string created_at: string id: string - key: string name: string - owner_user_id: string - updated_at: string | null + updated_at: string } Insert: { - app_id: string - company_id: string + api_key: string created_at?: string id?: string - key: string name: string - owner_user_id: string - updated_at?: string | null + updated_at?: string } Update: { - app_id?: string - company_id?: string + api_key?: string created_at?: string id?: string - key?: string name?: string - owner_user_id?: string - updated_at?: string | null + updated_at?: string } } apps: { Row: { - company_id: string created_at: string id: string name: string - owner_user_id: string - updated_at: string | null + updated_at: string } Insert: { - company_id: string created_at?: string id?: string name: string - owner_user_id: string - updated_at?: string | null + updated_at?: string } Update: { - company_id?: string created_at?: string id?: string name?: string - owner_user_id?: string - updated_at?: string | null + updated_at?: string + } + } + 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: { - app_id: string created_at: string id: string name: string - owner_user_id: string - updated_at: string | null + owner_user_id: string | null + updated_at: string } Insert: { - app_id: string created_at?: string id?: string name: string - owner_user_id: string - updated_at?: string | null + owner_user_id?: string | null + updated_at?: string } Update: { - app_id?: string created_at?: string id?: string name?: string - owner_user_id?: string - updated_at?: string | null + owner_user_id?: string | null + updated_at?: string } } companies: { @@ -98,47 +131,160 @@ export interface Database { created_at: string id: string name: string - owner_user_id: string - updated_at: string | null + updated_at: string } Insert: { created_at?: string id?: string name: string - owner_user_id: string - updated_at?: string | null + updated_at?: string } Update: { created_at?: string id?: string name?: string - owner_user_id?: string - updated_at?: string | null + updated_at?: string } } - messages: { + 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: { + 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 | null - user_id: string | null + updated_at: string } Insert: { - channel_id?: string | null created_at?: string id?: string message: string - updated_at?: string | null - user_id?: string | null + updated_at?: string } Update: { - channel_id?: string | null created_at?: string id?: string message?: string - updated_at?: string | null + 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 } } @@ -161,24 +307,21 @@ export interface Database { } users: { Row: { - app_id: string | null created_at: string id: string - updated_at: string | null + updated_at: string username: string } Insert: { - app_id?: string | null created_at?: string id?: string - updated_at?: string | null + updated_at?: string username: string } Update: { - app_id?: string | null created_at?: string id?: string - updated_at?: string | null + updated_at?: string username?: string } } diff --git a/src/types.ts b/src/types.ts index 9ea955f..ee474f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,8 +37,9 @@ export interface User { export interface Channel { id: string; name: string; - owner_user_id: string; + owner_user_id: string | null; created_at: string; + updated_at:string; participants?: User[]; } diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 40310be..568f0d6 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -27,10 +27,33 @@ function verifyToken(token:string) { } + +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 apps and companies are secure - const nonSecureRoutes = ['/get-chat-token','/get-server-api-key',"/apps","/companies"] - if (nonSecureRoutes.includes(req.path)){ + // 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) { @@ -42,37 +65,95 @@ export const secureClientRoutesWithJWTs = async (req:Request, res:Response, next 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,companyID} = keyDetails + 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 +} - try{ - const {data, error } = await supabase - .from('api_keys') - .upsert({ key: newKey,app_id:appID,company_id:companyID,owner_user_id:userID,name:"new API key"}) - .eq('app_id', appID) - if (error){ - console.log(error) - } - else{ - console.log(data) - return newKey - } - }catch(err){ - console.log(err) +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_keys') - .select('key') + .from('api_key_app') + .select(`api_keys( + api_key + )`) .eq('app_id', appID) - .eq('key', receivedAPIKey) if (error){ console.log(error) return false @@ -82,12 +163,8 @@ export const isValidAPIKey = async function (appID:string, receivedAPIKey:string console.log("No api key found") return false } - if (data.length > 1) { - console.log("MORE THAN ONE API KEY FOUND") - return false - } - const storedAPIKey = data[0].key - return receivedAPIKey === storedAPIKey + const isAuthenticated = data.some((item:any) => item.api_keys.api_key === receivedAPIKey) + return isAuthenticated } }catch(err){ return false