Working version of whole form
This commit is contained in:
parent
3741b37ff4
commit
eb2752c4e0
20 changed files with 625 additions and 42 deletions
|
@ -6,6 +6,7 @@
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/base": "^5.0.0-beta.8",
|
"@mui/base": "^5.0.0-beta.8",
|
||||||
|
"@mui/icons-material": "^5.14.1",
|
||||||
"@mui/material": "^5.14.2",
|
"@mui/material": "^5.14.2",
|
||||||
"@types/node": "^16.18.39",
|
"@types/node": "^16.18.39",
|
||||||
"@types/react": "^18.2.16",
|
"@types/react": "^18.2.16",
|
||||||
|
|
29
client/src/api/bridge.ts
Normal file
29
client/src/api/bridge.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
export const getBridgeByStateToken = (callback: CallableFunction) => {
|
||||||
|
fetch('api/bridge')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
console.log('bridge result', res)
|
||||||
|
if (res.status === 'success'){
|
||||||
|
console.log('successful get bridge')
|
||||||
|
callback(res.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setBridgeLabel = (label:string, callback: CallableFunction) => {
|
||||||
|
fetch('api/bridge',{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
Label: label
|
||||||
|
})
|
||||||
|
}).then(res => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === "success") {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
22
client/src/api/slack.ts
Normal file
22
client/src/api/slack.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export const getSlackInstallURL = (callback: CallableFunction) => {
|
||||||
|
fetch('api/slack/install')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
console.log('Got slack url', res);
|
||||||
|
callback(res.data.url, res.data.state_token)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getSlackChannels = (callback: CallableFunction) => {
|
||||||
|
fetch('api/slack/channels')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
console.log('Got slack channels', res);
|
||||||
|
if (res.status === "success"){
|
||||||
|
callback(res.data.channels.sort())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
26
client/src/components/join/joinBridge.tsx
Normal file
26
client/src/components/join/joinBridge.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import {Grid} from "@mui/material";
|
||||||
|
import {TextField} from "@mui/material";
|
||||||
|
|
||||||
|
|
||||||
|
export const JoinBridge = ({
|
||||||
|
bridge, setBridge
|
||||||
|
}) => {
|
||||||
|
const onSetLabel = (label) => {
|
||||||
|
setBridge({...bridge, Label:label})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2} columns={2}>
|
||||||
|
<Grid item xs={1}>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={1}>
|
||||||
|
<TextField
|
||||||
|
onChange={(event) => {onSetLabel(event.target.value)}}>
|
||||||
|
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -1,25 +1,166 @@
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Accordion from '@mui/material/Accordion';
|
||||||
|
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||||
|
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||||
|
|
||||||
|
|
||||||
import {Group} from "../../types/group";
|
import {Group} from "../../types/group";
|
||||||
|
import {JoinStep} from './joinStep';
|
||||||
import {JoinPlatform} from "./joinPlatform";
|
import {JoinPlatform} from "./joinPlatform";
|
||||||
import {useState} from "react";
|
import {useState, useEffect} from "react";
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import {setBridgeLabel} from "../../api/bridge";
|
||||||
|
import {getSlackChannels} from "../../api/slack";
|
||||||
|
import FormControl from "@mui/material/FormControl";
|
||||||
|
import Select from "@mui/material/Select"
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
|
||||||
export interface JoinFormProps {
|
export interface JoinFormProps {
|
||||||
group: Group
|
group: Group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface stepCompleteType {
|
||||||
|
login: boolean;
|
||||||
|
bridge: boolean;
|
||||||
|
channel: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const JoinForm = ({group}: JoinFormProps) => {
|
export const JoinForm = ({group}: JoinFormProps) => {
|
||||||
const [platform, setPlatform] = useState();
|
const [platform, setPlatform] = useState<string>();
|
||||||
|
const [channels, setChannels] = useState<string[]>();
|
||||||
|
const [selectedChannel, setSelectedChannel] = useState<string>();
|
||||||
|
const [bridge, setBridge] = useState<{
|
||||||
|
Label: string;
|
||||||
|
Protocol: string;
|
||||||
|
team_name: string;
|
||||||
|
}>();
|
||||||
|
const [stepComplete, setStepComplete] = useState<stepCompleteType>({
|
||||||
|
login: false,
|
||||||
|
bridge: false,
|
||||||
|
channel: false
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bridge !== undefined){
|
||||||
|
setStepComplete({...stepComplete, login:true})
|
||||||
|
}
|
||||||
|
if (channels === undefined){
|
||||||
|
getSlackChannels(setChannels)
|
||||||
|
}
|
||||||
|
}, [bridge])
|
||||||
|
|
||||||
|
const onSetBridge = (evt) => {
|
||||||
|
setBridge({...bridge, Label:evt.target.value})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBridgeLabel = () => {
|
||||||
|
setBridgeLabel(bridge.Label, () => {setStepComplete({...stepComplete, bridge:true})})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectChannel = (evt) => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinChannel = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className={'section-header'}>
|
<header className={'section-header'}>
|
||||||
Joining group: <code>{group.name}</code>
|
Joining group: <code>{group.name}</code>
|
||||||
</header>
|
</header>
|
||||||
<JoinPlatform
|
<JoinStep
|
||||||
platformSetter = {setPlatform}
|
title={"1) Login"}
|
||||||
></JoinPlatform>
|
details={"Select your chat platform"}
|
||||||
</>
|
id={'login'}
|
||||||
|
completed={stepComplete.login}
|
||||||
|
>
|
||||||
|
<JoinPlatform
|
||||||
|
platformSetter = {setPlatform}
|
||||||
|
bridgeSetter = {setBridge}
|
||||||
|
completeSetter= {setStepComplete}
|
||||||
|
/>
|
||||||
|
</JoinStep>
|
||||||
|
<JoinStep
|
||||||
|
title={"2) Set Bridge Label"}
|
||||||
|
details={"A short identifier shown before your messages"}
|
||||||
|
id={'bridge'}
|
||||||
|
disabled={!stepComplete.login}
|
||||||
|
completed={stepComplete.bridge}
|
||||||
|
>
|
||||||
|
<Grid container spacing={2} columns={4} alignItems="center">
|
||||||
|
<Grid item xs={1}>
|
||||||
|
<Typography>Label:</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2}>
|
||||||
|
<TextField
|
||||||
|
value={bridge?.Label}
|
||||||
|
onChange={onSetBridge}>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={1}>
|
||||||
|
<Button
|
||||||
|
variant={"outlined"}
|
||||||
|
onClick={updateBridgeLabel}>
|
||||||
|
Update Label!
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</JoinStep>
|
||||||
|
<JoinStep
|
||||||
|
title={"3) Select a channel!"}
|
||||||
|
details={"The bot will join :)"}
|
||||||
|
id={'bridge'}
|
||||||
|
disabled={!stepComplete.login}
|
||||||
|
completed={stepComplete.channel}
|
||||||
|
>
|
||||||
|
<div className={"list-row"}>
|
||||||
|
<FormControl sx={{width: "50%"}}>
|
||||||
|
<InputLabel>Select Platform</InputLabel>
|
||||||
|
<Select
|
||||||
|
// value={platform}
|
||||||
|
onChange={(evt:any) => {setSelectedChannel(evt.target.value)}}
|
||||||
|
label={"Select Channel"}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
channels ?
|
||||||
|
channels.map(chan => {
|
||||||
|
return(<MenuItem value={chan} key={chan}>{chan}</MenuItem>)
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
variant={"outlined"}
|
||||||
|
onClick={joinChannel}>
|
||||||
|
Join Channel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Grid container spacing={2} columns={2} alignItems="center">
|
||||||
|
<Grid item xs={1}>
|
||||||
|
<Typography>Label:</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2}>
|
||||||
|
<TextField
|
||||||
|
value={bridge?.Label}
|
||||||
|
onChange={onSetBridge}>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={1}>
|
||||||
|
<Button
|
||||||
|
variant={"outlined"}
|
||||||
|
onClick={updateBridgeLabel}>
|
||||||
|
Update Label!
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</JoinStep>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -2,42 +2,125 @@
|
||||||
Select which platform you're joining from!
|
Select which platform you're joining from!
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState} from 'react';
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
import Select from '@mui/material/Select';
|
import Select from '@mui/material/Select';
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
|
||||||
|
import {getSlackInstallURL} from "../../api/slack";
|
||||||
|
import {getBridgeByStateToken} from "../../api/bridge";
|
||||||
import {JoinSlack} from "../panels/joinSlack";
|
import {JoinSlack} from "../panels/joinSlack";
|
||||||
|
|
||||||
const PLATFORMS = {
|
enum PLATFORMS {
|
||||||
'Slack': JoinSlack
|
Slack = 'Slack'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JoinPlatform = ({
|
export const JoinPlatform = ({
|
||||||
platformSetter
|
platformSetter,
|
||||||
|
bridgeSetter,
|
||||||
|
completeSetter
|
||||||
}) => {
|
}) => {
|
||||||
const [platform, setPlatform] = useState()
|
const [platform, setPlatform] = useState<PLATFORMS>();
|
||||||
|
const [installLink, setInstallLink] = useState();
|
||||||
|
const [stateToken, setStateToken] = useState();
|
||||||
|
const [bridge, setBridge] = useState();
|
||||||
|
|
||||||
|
const pingTimeout = useRef(null);
|
||||||
|
|
||||||
const handleSelect = (event) => {
|
const handleSelect = (event) => {
|
||||||
setPlatform(event.target.value);
|
setPlatform(event.target.value);
|
||||||
platformSetter(event.target.value);
|
platformSetter(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (platform === "Slack") {
|
||||||
|
console.log('Getting slack URL')
|
||||||
|
getSlackInstallURL(handleInstallLink)
|
||||||
|
}
|
||||||
|
}, [platform])
|
||||||
|
|
||||||
|
const handleInstallLink = (url, state_token) => {
|
||||||
|
setInstallLink(url);
|
||||||
|
setStateToken(state_token);
|
||||||
|
// pingForBridge()
|
||||||
|
}
|
||||||
|
|
||||||
|
// const pingForBridge = () =>{
|
||||||
|
// if (bridge === undefined){
|
||||||
|
// console.log('bridge is', bridge)
|
||||||
|
// getBridgeByStateToken(setBridge);
|
||||||
|
// setTimeout(pingForBridge, 1000);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const pingForBridge = () => {
|
||||||
|
if (bridge === undefined) {
|
||||||
|
console.log('bridge is', bridge);
|
||||||
|
getBridgeByStateToken(setBridge);
|
||||||
|
|
||||||
|
pingTimeout.current = setTimeout(pingForBridge, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stateToken !== undefined) {
|
||||||
|
if (bridge === undefined){
|
||||||
|
pingForBridge()
|
||||||
|
// pingTimeout.current = setInterval(pingForBridge, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return () => {clearInterval(pingTimeout.current)}
|
||||||
|
|
||||||
|
// if (stateToken !== undefined){
|
||||||
|
// if (bridge === undefined){
|
||||||
|
// console.log('bridge is', bridge)
|
||||||
|
// pingTimeout.current = setTimeout(() => {
|
||||||
|
// getBridgeByStateToken(setBridge);
|
||||||
|
// }, 1000)
|
||||||
|
// // setTimeout(pingForBridge, 1000);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
}, [stateToken, bridge])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
bridgeSetter(bridge)
|
||||||
|
}, [bridge])
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
//
|
||||||
|
// }, [installLink])
|
||||||
|
|
||||||
|
const openInstallTab = () => {
|
||||||
|
|
||||||
|
window.open(installLink, '_blank').focus();
|
||||||
|
}
|
||||||
|
|
||||||
return(
|
return(
|
||||||
<div className={"list-row"}>
|
<div className={"list-row"}>
|
||||||
<FormControl fullWidth>
|
<FormControl sx={{width: "50%"}}>
|
||||||
<InputLabel>Select Platform</InputLabel>
|
<InputLabel>Select Platform</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={platform}
|
// value={platform}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
label={"Select Platform"}
|
label={"Select Platform"}
|
||||||
>
|
>
|
||||||
<MenuItem value={'slack'}>Slack</MenuItem>
|
<MenuItem value={'Slack'}>Slack</MenuItem>
|
||||||
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{
|
||||||
|
installLink && platform == "Slack" ?
|
||||||
|
<Button
|
||||||
|
variant={"outlined"}
|
||||||
|
onClick={openInstallTab}>
|
||||||
|
Add to Slack
|
||||||
|
</Button>
|
||||||
|
: <div style={{width: "50%"}}></div>
|
||||||
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
60
client/src/components/join/joinStep.tsx
Normal file
60
client/src/components/join/joinStep.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import Accordion from '@mui/material/Accordion';
|
||||||
|
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||||
|
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked';
|
||||||
|
import TaskAltIcon from '@mui/icons-material/TaskAlt';
|
||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
|
export interface JoinStepProps {
|
||||||
|
children: any;
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
details?: string;
|
||||||
|
completed?: boolean;
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function JoinStep(
|
||||||
|
{
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
title = '',
|
||||||
|
details = '',
|
||||||
|
completed = false,
|
||||||
|
disabled = false
|
||||||
|
}: JoinStepProps){
|
||||||
|
|
||||||
|
return(
|
||||||
|
<Accordion
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
aria-controls="panel1bh-content"
|
||||||
|
id="panel1bh-header"
|
||||||
|
>
|
||||||
|
<Typography sx={{ width: '33%', flexShrink: 0 }}>
|
||||||
|
{ title }
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: 'text.secondary', flexGrow: 1 }}>
|
||||||
|
{ details }
|
||||||
|
</Typography>
|
||||||
|
{
|
||||||
|
completed ?
|
||||||
|
<TaskAltIcon color={"success"}/>
|
||||||
|
:
|
||||||
|
<RadioButtonUncheckedIcon/>
|
||||||
|
}
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Typography>
|
||||||
|
{ children }
|
||||||
|
</Typography>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -1,3 +1,19 @@
|
||||||
|
.list-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2em;
|
||||||
|
|
||||||
|
&>div {
|
||||||
|
flex-basis: 50%;
|
||||||
|
}
|
||||||
|
.MuiButton-root {
|
||||||
|
flex-basis: 50%;
|
||||||
|
flex-grow: 0;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.InputSlot {
|
.InputSlot {
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
.section-header {
|
.section-header {
|
||||||
color: $color-text;
|
color: $color-text;
|
||||||
font-size: 2rem;
|
font: {
|
||||||
|
size: 1.5rem;
|
||||||
|
weight: bold;
|
||||||
|
}
|
||||||
margin: {
|
margin: {
|
||||||
top: 1rem;
|
top: 2rem;
|
||||||
|
bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ POSTGRES_DB=
|
||||||
SLACK_CLIENT_ID=
|
SLACK_CLIENT_ID=
|
||||||
SLACK_CLIENT_SECRET=
|
SLACK_CLIENT_SECRET=
|
||||||
SLACK_SIGNING_SECRET=
|
SLACK_SIGNING_SECRET=
|
||||||
|
SLACK_STATE_SECRET=
|
||||||
|
|
||||||
ADMIN_TOKEN=
|
ADMIN_TOKEN=
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ export default {
|
||||||
slackConfig: {
|
slackConfig: {
|
||||||
client_id: 'SLACK_CLIENT_ID',
|
client_id: 'SLACK_CLIENT_ID',
|
||||||
client_secret: 'SLACK_CLIENT_SECRET',
|
client_secret: 'SLACK_CLIENT_SECRET',
|
||||||
signing_secret: 'SLACK_SIGNING_SECRET'
|
signing_secret: 'SLACK_SIGNING_SECRET',
|
||||||
|
state_secret: 'SLACK_STATE_SECRET'
|
||||||
},
|
},
|
||||||
admin_token: 'ADMIN_TOKEN',
|
admin_token: 'ADMIN_TOKEN',
|
||||||
cookies:{
|
cookies:{
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
import express, { NextFunction, Request, Response } from 'express';
|
import express, { NextFunction, Request, Response } from 'express';
|
||||||
import config from 'config';
|
import config from 'config';
|
||||||
|
@ -13,6 +11,7 @@ import {cookieMiddleware} from "./middleware/cookies";
|
||||||
import groupRoutes from "./routes/group.routes";
|
import groupRoutes from "./routes/group.routes";
|
||||||
import slackRoutes from "./routes/slack.routes";
|
import slackRoutes from "./routes/slack.routes";
|
||||||
import authRoutes from "./routes/auth.routes";
|
import authRoutes from "./routes/auth.routes";
|
||||||
|
import bridgeRoutes from './routes/bridge.routes';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,6 +39,7 @@ AppDataSource.initialize()
|
||||||
// });
|
// });
|
||||||
|
|
||||||
app.use('/slack', slackRoutes);
|
app.use('/slack', slackRoutes);
|
||||||
|
app.use('/bridge', bridgeRoutes)
|
||||||
|
|
||||||
app.all('*', (req: Request, res: Response, next: NextFunction) => {
|
app.all('*', (req: Request, res: Response, next: NextFunction) => {
|
||||||
next(new AppError(404, `Route ${req.originalUrl} not found`));
|
next(new AppError(404, `Route ${req.originalUrl} not found`));
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import {AppDataSource} from "../db/data-source";
|
||||||
|
import {Bridge} from "../entities/bridge.entity";
|
||||||
|
import {Request, Response} from "express";
|
||||||
|
import {UpdateBridgeInput} from "../schemas/bridge.schema";
|
||||||
|
|
||||||
|
const bridgeRepository = AppDataSource.getRepository(Bridge)
|
||||||
|
|
||||||
|
|
||||||
|
export const getBridgeHandler = async(
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
if (req.session.state_token){
|
||||||
|
let bridge = await bridgeRepository.findOne({
|
||||||
|
where: {state_token: req.session.state_token},
|
||||||
|
select: {
|
||||||
|
Protocol: true,
|
||||||
|
Label: true,
|
||||||
|
team_name: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!bridge){
|
||||||
|
res.status(404).json({
|
||||||
|
status: 'failure',
|
||||||
|
message: 'No matching bridge found'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
data: bridge
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.status(403).json({
|
||||||
|
status: 'failure',
|
||||||
|
message: 'No state token found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setBridgeHandler = async(
|
||||||
|
req: Request<{}, {}, UpdateBridgeInput>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
if (req.session.state_token) {
|
||||||
|
let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
|
||||||
|
if (!bridge){
|
||||||
|
res.status(404).json({
|
||||||
|
status: 'failure',
|
||||||
|
message: 'No matching bridge found'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bridge.Label = req.body.Label;
|
||||||
|
bridge.save()
|
||||||
|
res.status(200).json({
|
||||||
|
status:'success'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.status(403).json({
|
||||||
|
status: 'failure',
|
||||||
|
message: 'No state token found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,11 +11,13 @@ const scopes = ['bot', 'channels:write', 'chat:write:bot', 'chat:write:user', 'u
|
||||||
const bridgeRepository = AppDataSource.getRepository(Bridge)
|
const bridgeRepository = AppDataSource.getRepository(Bridge)
|
||||||
const groupRepository = AppDataSource.getRepository(Group)
|
const groupRepository = AppDataSource.getRepository(Group)
|
||||||
|
|
||||||
|
const SLACK_COOKIE_NAME = "slack-oauth-state";
|
||||||
|
|
||||||
const slackConfig = config.get<{
|
const slackConfig = config.get<{
|
||||||
client_id: string,
|
client_id: string,
|
||||||
client_secret: string,
|
client_secret: string,
|
||||||
signing_secret: string
|
signing_secret: string,
|
||||||
|
state_secret: string
|
||||||
}>('slackConfig');
|
}>('slackConfig');
|
||||||
|
|
||||||
const installer = new InstallProvider({
|
const installer = new InstallProvider({
|
||||||
|
@ -23,9 +25,11 @@ const installer = new InstallProvider({
|
||||||
clientSecret: slackConfig.client_secret,
|
clientSecret: slackConfig.client_secret,
|
||||||
authVersion: 'v1',
|
authVersion: 'v1',
|
||||||
scopes,
|
scopes,
|
||||||
stateSecret: randomUUID(),
|
// stateSecret: slackConfig.state_secret,
|
||||||
installationStore: new FileInstallationStore(),
|
stateVerification: false,
|
||||||
|
// installationStore: new FileInstallationStore(),
|
||||||
logLevel: LogLevel.DEBUG,
|
logLevel: LogLevel.DEBUG,
|
||||||
|
// stateCookieName: SLACK_COOKIE_NAME
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -45,18 +49,24 @@ export const SlackInstallLinkHandler = async(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let login_token = randomUUID()
|
let state_token = randomUUID()
|
||||||
|
req.session.state_token = state_token;
|
||||||
|
|
||||||
const url = await installer.generateInstallUrl({
|
const url = await installer.generateInstallUrl(
|
||||||
scopes,
|
{
|
||||||
metadata: {token: login_token, group: req.query.group}
|
scopes,
|
||||||
});
|
metadata: {token: state_token, group: req.query.group}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
state_token
|
||||||
|
);
|
||||||
|
|
||||||
|
res.cookie(SLACK_COOKIE_NAME, state_token, { maxAge: 60*5 })
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: {
|
data: {
|
||||||
url,
|
url,
|
||||||
login_token
|
state_token
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -72,23 +82,36 @@ export const SlackCallbackHandler = async(
|
||||||
// using custom success and failure handlers
|
// using custom success and failure handlers
|
||||||
const callbackOptions = {
|
const callbackOptions = {
|
||||||
success: async (installation, installOptions, req, res) => {
|
success: async (installation, installOptions, req, res) => {
|
||||||
console.log(installation, installOptions, req.body, req.content, req.query, req.params)
|
// console.log(installation, installOptions, req.body, req.content, req.query, req.params)
|
||||||
console.log(installation.team.id, installation.team.name, installation.bot.token);
|
// console.log(installation.team.id, installation.team.name, installation.bot.token);
|
||||||
let bridge = await bridgeRepository.create({
|
let bridge_data = {
|
||||||
'Protocol': 'slack',
|
'Protocol': 'slack',
|
||||||
'Label': installation.metadata.name,
|
'Label': installation.team.name,
|
||||||
'team_id': installation.team.id,
|
'team_id': installation.team.id,
|
||||||
'team_name': installation.team.name,
|
'team_name': installation.team.name,
|
||||||
|
'state_token': req.session.state_token,
|
||||||
'Token': installation.bot.token
|
'Token': installation.bot.token
|
||||||
});
|
}
|
||||||
let result = await bridgeRepository.save(bridge);
|
|
||||||
|
|
||||||
|
// check if we have an entity
|
||||||
res.send(result);
|
let bridge = await bridgeRepository.findOneBy({Token: installation.bot.token})
|
||||||
|
let result
|
||||||
|
if (!bridge){
|
||||||
|
bridge = await bridgeRepository.create(bridge_data);
|
||||||
|
await bridgeRepository.save(bridge);
|
||||||
|
console.log('created bridge')
|
||||||
|
} else {
|
||||||
|
await bridgeRepository.update(
|
||||||
|
{Token: installation.bot.token},
|
||||||
|
{state_token: req.session.state_token})
|
||||||
|
console.log('updated bridge')
|
||||||
|
}
|
||||||
|
res.send('<html><body><h1>Success! Return to the chatbridge login window.</h1><h3>This tab will close in 3 seconds...</h3><script>window.setTimeout(window.close, 3000)</script></body>')
|
||||||
|
|
||||||
},
|
},
|
||||||
failure: (error, installOptions , req, res) => {
|
failure: (error, installOptions , req, res) => {
|
||||||
res.send('failure');
|
console.log(error, installOptions, req.body, req.content, req.query, req.params)
|
||||||
|
res.send('failure. Something is broken about chatbridge :(');
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,3 +119,41 @@ export const SlackCallbackHandler = async(
|
||||||
await installer.handleCallback(req, res, callbackOptions);
|
await installer.handleCallback(req, res, callbackOptions);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getChannelsHandler = async(
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
|
||||||
|
console.log('bridge data', bridge)
|
||||||
|
|
||||||
|
try {
|
||||||
|
fetch('https://slack.com/api/conversations.list', {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${bridge.Token}`
|
||||||
|
}
|
||||||
|
}).then(result => result.json())
|
||||||
|
.then((result) => {
|
||||||
|
console.log('channels',res)
|
||||||
|
let channels = result.channels.map(
|
||||||
|
(chan: { name: string; }) => {
|
||||||
|
return chan.name
|
||||||
|
}
|
||||||
|
);
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
channels
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return res.status(502).json({
|
||||||
|
status: 'failure',
|
||||||
|
message: 'couldnt get lists!'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,10 @@ export class Bridge extends Model {
|
||||||
@Column({nullable:true})
|
@Column({nullable:true})
|
||||||
team_name: string;
|
team_name: string;
|
||||||
|
|
||||||
|
// Used to fetch the bridge data from the client while installing
|
||||||
|
@Column()
|
||||||
|
state_token: string;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
unique: true
|
unique: true
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,6 +2,11 @@ import cookieSession from 'cookie-session';
|
||||||
import config from 'config';
|
import config from 'config';
|
||||||
import {Request, Response, NextFunction} from "express";
|
import {Request, Response, NextFunction} from "express";
|
||||||
import { tokenHasher, hashed_token } from "../auth";
|
import { tokenHasher, hashed_token } from "../auth";
|
||||||
|
import {AppDataSource} from "../db/data-source";
|
||||||
|
import {Bridge} from "../entities/bridge.entity";
|
||||||
|
|
||||||
|
const bridgeRepository = AppDataSource.getRepository(Bridge)
|
||||||
|
|
||||||
|
|
||||||
const cookieConfig = config.get<{
|
const cookieConfig = config.get<{
|
||||||
'key1': string,
|
'key1': string,
|
||||||
|
@ -33,3 +38,21 @@ export const requireAdmin = (req: Request, res: Response, next: NextFunction) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const requireStateToken = async(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
if (req.session.state_token) {
|
||||||
|
let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
|
||||||
|
if (!bridge){
|
||||||
|
return res.status(404).json({
|
||||||
|
status: 'failure',
|
||||||
|
message: 'No matching bridge found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
return res.status(403).json({
|
||||||
|
status: 'failure',
|
||||||
|
message: 'No state token found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
20
server/src/routes/bridge.routes.ts
Normal file
20
server/src/routes/bridge.routes.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getBridgeHandler,
|
||||||
|
setBridgeHandler
|
||||||
|
} from "../controllers/bridge.controller";
|
||||||
|
|
||||||
|
import {
|
||||||
|
updateBridgeSchema
|
||||||
|
} from "../schemas/bridge.schema";
|
||||||
|
|
||||||
|
import { validate } from "../middleware/validate";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.route('/')
|
||||||
|
.get(getBridgeHandler)
|
||||||
|
.post(validate(updateBridgeSchema), setBridgeHandler)
|
||||||
|
|
||||||
|
export default router
|
|
@ -1,16 +1,24 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SlackInstallHandler,
|
SlackInstallLinkHandler,
|
||||||
SlackCallbackHandler
|
SlackCallbackHandler,
|
||||||
|
getChannelsHandler
|
||||||
} from '../controllers/slack.controller'
|
} from '../controllers/slack.controller'
|
||||||
|
|
||||||
|
import {
|
||||||
|
requireStateToken
|
||||||
|
} from "../middleware/cookies";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.route('/install')
|
router.route('/install')
|
||||||
.get(SlackInstallHandler)
|
.get(SlackInstallLinkHandler)
|
||||||
|
|
||||||
router.route('/oauth_redirect')
|
router.route('/oauth_redirect')
|
||||||
.get(SlackCallbackHandler)
|
.get(SlackCallbackHandler)
|
||||||
|
|
||||||
|
router.route('/channels')
|
||||||
|
.get(requireStateToken, getChannelsHandler)
|
||||||
|
|
||||||
export default router
|
export default router
|
11
server/src/schemas/bridge.schema.ts
Normal file
11
server/src/schemas/bridge.schema.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { object, string, TypeOf } from 'zod';
|
||||||
|
|
||||||
|
export const updateBridgeSchema = object({
|
||||||
|
body: object({
|
||||||
|
Label: string({
|
||||||
|
required_error: "Label for bridge required to update bridge"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export type UpdateBridgeInput = TypeOf<typeof updateBridgeSchema>['body'];
|
|
@ -1942,6 +1942,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.2.tgz#d8fcacdb1d37e621fce33ea808180fa5a590f908"
|
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.2.tgz#d8fcacdb1d37e621fce33ea808180fa5a590f908"
|
||||||
integrity sha512-x+c/MgDL1t/IIy5lDbMlrDouFG5DYZbl3DP4dbbuhlpPFBnE9glYwmJEee/orVHQpOPwLxCAIWQs+2DKSaBVWQ==
|
integrity sha512-x+c/MgDL1t/IIy5lDbMlrDouFG5DYZbl3DP4dbbuhlpPFBnE9glYwmJEee/orVHQpOPwLxCAIWQs+2DKSaBVWQ==
|
||||||
|
|
||||||
|
"@mui/icons-material@^5.14.1":
|
||||||
|
version "5.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.14.1.tgz#2f145c15047a0c7f01353ce620cb88276dadba9e"
|
||||||
|
integrity sha512-xV/f26muQqtWzerzOIdGPrXoxp/OKaE2G2Wp9gnmG47mHua5Slup/tMc3fA4ZYUreGGrK6+tT81TEvt1Wsng8Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.22.6"
|
||||||
|
|
||||||
"@mui/material@^5.14.2":
|
"@mui/material@^5.14.2":
|
||||||
version "5.14.2"
|
version "5.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.14.2.tgz#13b113489a61021145d62e0383912ca487a46375"
|
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.14.2.tgz#13b113489a61021145d62e0383912ca487a46375"
|
||||||
|
|
Loading…
Reference in a new issue