mobile styles and info panel
This commit is contained in:
parent
48028193cc
commit
bcc032413c
20 changed files with 280 additions and 56 deletions
|
@ -1,26 +1,68 @@
|
|||
import React from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import { yellow } from "@mui/material/colors";
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import ModePanel from "./components/modePanel";
|
||||
import Button from "@mui/material/Button";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
|
||||
import InfoModal from "./components/modals/infoModal";
|
||||
import {Info} from "@mui/icons-material";
|
||||
|
||||
const REPO_URL = "https://github.com/sneakers-the-rat/chatbridge"
|
||||
|
||||
|
||||
const theme = createTheme({
|
||||
palette:{
|
||||
primary: yellow,
|
||||
mode: "dark"
|
||||
mode: "dark",
|
||||
}
|
||||
})
|
||||
|
||||
import ModePanel from "./components/modePanel";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
|
||||
const handleInfo = () => {
|
||||
setInfoOpen(!infoOpen)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<div className={"App-Container"}>
|
||||
<div className="App">
|
||||
<div className={"App-header-bar"}>
|
||||
<header className="App-header">
|
||||
ChatBridge
|
||||
</header>
|
||||
<div className={"header-padding"}/>
|
||||
<Button
|
||||
className={"App-header-item"}
|
||||
variant="contained"
|
||||
aria-label={"source code"}
|
||||
color={"primary"}
|
||||
onClick={handleInfo}
|
||||
>
|
||||
INFO
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
aria-label={"source code"}
|
||||
color={"primary"}
|
||||
href={REPO_URL}
|
||||
>
|
||||
<GitHubIcon/>
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
<ModePanel/>
|
||||
<InfoModal open={infoOpen} setOpen={setInfoOpen}/>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
|
|
|
@ -3,9 +3,7 @@ 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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -22,8 +22,8 @@ export default function GroupPanel({
|
|||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Group</TableCell>
|
||||
<TableCell align="right">Created At</TableCell>
|
||||
<TableCell align="right">Invite Token</TableCell>
|
||||
<TableCell align="right">Created At</TableCell>
|
||||
<TableCell align="right">Delete</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
|
|
@ -32,8 +32,8 @@ export default function GroupRow(
|
|||
<TableCell component="th" scope="row">
|
||||
{name}
|
||||
</TableCell>
|
||||
<TableCell align="right">{created_at}</TableCell>
|
||||
<TableCell align="right">{invite_token}</TableCell>
|
||||
<TableCell align="right">{created_at}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={handleDeleteGroup}
|
||||
|
|
|
@ -36,7 +36,7 @@ const JoinChannel = ({
|
|||
|
||||
|
||||
useEffect(() => {
|
||||
if (bridge){
|
||||
if (bridge && !channels){
|
||||
switch(platform){
|
||||
case "Slack":
|
||||
getSlackChannels(setChannels)
|
||||
|
|
|
@ -59,11 +59,11 @@ export const JoinForm = ({group}: JoinFormProps) => {
|
|||
return (
|
||||
<>
|
||||
<header className={'section-header'}>
|
||||
Joining group: <code>{group.name}</code>
|
||||
<code>{group.name}</code>
|
||||
</header>
|
||||
<JoinStep
|
||||
title={"1) Login"}
|
||||
details={"Select your chat platform"}
|
||||
// details={"Select your chat platform"}
|
||||
id={'login'}
|
||||
completed={stepComplete.login}
|
||||
>
|
||||
|
@ -78,7 +78,7 @@ export const JoinForm = ({group}: JoinFormProps) => {
|
|||
</JoinStep>
|
||||
<JoinStep
|
||||
title={"2) Configure Bridge"}
|
||||
details={"Settings for all channels bridged from this platform"}
|
||||
// details={"Settings for all channels bridged from this platform"}
|
||||
id={'bridge'}
|
||||
disabled={!stepComplete.login}
|
||||
completed={stepComplete.bridge}
|
||||
|
@ -92,7 +92,7 @@ export const JoinForm = ({group}: JoinFormProps) => {
|
|||
</JoinStep>
|
||||
<JoinStep
|
||||
title={"3) Select a channel!"}
|
||||
details={"The bot will join :)"}
|
||||
// details={"The bot will join :)"}
|
||||
id={'channel'}
|
||||
disabled={!stepComplete.login}
|
||||
completed={stepComplete.channel}
|
||||
|
|
|
@ -29,7 +29,6 @@ export const JoinGroup = ({
|
|||
setAuthError(false);
|
||||
setErrorText('');
|
||||
setGroup(response.data)
|
||||
console.log(response)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,18 +30,31 @@ export function JoinStep(
|
|||
return(
|
||||
<Accordion
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel1bh-content"
|
||||
id="panel1bh-header"
|
||||
>
|
||||
<Typography sx={{ width: '33%', flexShrink: 0 }}>
|
||||
<Typography sx={details === '' ?
|
||||
{
|
||||
width:'66%',
|
||||
flexGrow: 1
|
||||
}
|
||||
:
|
||||
{
|
||||
width:'33%',
|
||||
flexShrink: 0
|
||||
}
|
||||
}>
|
||||
{ title }
|
||||
</Typography>
|
||||
{details !== '' ?
|
||||
<Typography sx={{ color: 'text.secondary', flexGrow: 1 }}>
|
||||
{ details }
|
||||
</Typography>
|
||||
: undefined}
|
||||
{
|
||||
completed ?
|
||||
<TaskAltIcon color={"success"}/>
|
||||
|
@ -50,9 +63,7 @@ export function JoinStep(
|
|||
}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography>
|
||||
{ children }
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
|
|
82
client/src/components/modals/infoModal.tsx
Normal file
82
client/src/components/modals/infoModal.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import * as React from "react";
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
|
||||
export interface InfoModalProps {
|
||||
open: boolean
|
||||
setOpen: CallableFunction
|
||||
}
|
||||
|
||||
const InfoModal = ({
|
||||
open,
|
||||
setOpen
|
||||
}: InfoModalProps) => {
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
|
||||
return(
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
scroll={'body'}
|
||||
>
|
||||
<DialogTitle>
|
||||
ChatBridge INFO
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
ChatBridge is a tool for making groupchats that span multiple chat protocols and platforms.
|
||||
|
||||
It is a web frontend for <a href={"https://github.com/42wim/matterbridge"}>matterbridge</a> and manages the API logins for platforms like Slack and Discord to lower the barriers to
|
||||
bridging.
|
||||
<br/><br/>
|
||||
This is ALPHA software and functionality is not guaranteed! The eventual goal is to make <a href={"https://a.gup.pe/"}>guppe groups</a> for proprietary chat platforms, where anyone can create and join groups with just an invite token.
|
||||
For now, creating groups is limited to an administrator token until we can be more sure about the security and performance demands of running many matterbridge processes.
|
||||
</DialogContentText>
|
||||
<h3>Security & Privacy Notes</h3>
|
||||
<DialogContentText>
|
||||
This tool is made by privacy advocates and activists. The developers have no interest in storing or monetizing your information, full stop. We will <span style={{"fontStyle":"italic"}}>never</span> abuse the app tokens created by logging into the ChatBridge App for anything except configuring the underlying matterbridge processes
|
||||
<br/><br/>
|
||||
|
||||
To be as transparent as possible about the operation of the service:
|
||||
</DialogContentText>
|
||||
<h4>What is stored</h4>
|
||||
<DialogContentText>
|
||||
<ul>
|
||||
<li>Slack/Discord: App login tokens that are generated for your workspace/server</li>
|
||||
<li>Limited metadata about your bridged chat, specifically its name and unique ID (usually these are considered public anyway), and the short label you provide to the service</li>
|
||||
<li>The names of channels you have bridged</li>
|
||||
</ul>
|
||||
</DialogContentText>
|
||||
<h4>What is NOT stored</h4>
|
||||
<DialogContentText>
|
||||
<ul>
|
||||
<li>Message content and metadata of any kind, even in debugging logs</li>
|
||||
<li>No other metadata about your bridged chat except for that listed above - channel lists in the interface are requested and discarded during the API call</li>
|
||||
<li>The bot access tokens can NOT access DMs or private channels</li>
|
||||
</ul>
|
||||
</DialogContentText>
|
||||
<h4>Security of Data at Rest</h4>
|
||||
<DialogContentText>
|
||||
Matterbridge relies on <code>.toml</code> files that contain the app tokens in plain text. This sets a hard limit on how secure the data can be at rest - ie. encryption at rest is not possible. The tokens that are granted to ChatBridge CAN be used to exfiltrate chat history of public channels for slack and discord if they were to be lost in a data breach. We therfore do NOT recommend you use ChatBridge in a context where the contents of your public channels or the membership in your chatroom becoming public could pose a risk to your members.
|
||||
<br/><br/>
|
||||
For the main development instance, the service is run as its own user, and the configuration files are stored such that only that user can read them. For the data to be breached, an attacker would need to compromise a root SSH key.
|
||||
<br/><br/>
|
||||
For information accessed through the chatbridge interface, we authenticate using ephemeral signed cookies that expire 24 hours after they are issued.
|
||||
<br/><br/>
|
||||
We of course can make no guarantee about security of data at any other instances that deploy ChatBridge.
|
||||
<br/><br/>
|
||||
To revoke access to ChatBridge at any time, you can uninstall the app from your slack workspace or discord server - this makes the access keys obsolete and will require them to be reissued if you choose to rejoin ChatBridge.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default InfoModal
|
|
@ -30,8 +30,8 @@ export default function ModePanel() {
|
|||
onChange={handleChange}
|
||||
selectionFollowsFocus>
|
||||
{/*<TabsList className={"TabsList"}>*/}
|
||||
<Tab label={"Join Group"}></Tab>
|
||||
<Tab label={"Create Group"}></Tab>
|
||||
<Tab className={"Tab"} label={"Join Group"}></Tab>
|
||||
<Tab className={"Tab"} label={"Create Group"}></Tab>
|
||||
{/*</TabsList>*/}
|
||||
|
||||
</StyledTabs>
|
||||
|
|
|
@ -14,7 +14,8 @@ export default function TabPanel(props) {
|
|||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Box
|
||||
className={"TabPanel"}>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
//}
|
||||
border: 2px solid $color-primary;
|
||||
height:100%;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.App-Container {
|
||||
|
@ -23,6 +25,11 @@
|
|||
width:100%;
|
||||
height:100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -37,17 +44,49 @@
|
|||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
text-align: center;
|
||||
margin: {
|
||||
top: 1em;
|
||||
}
|
||||
.App-header-bar{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
flex-direction: row;
|
||||
padding: 0.5em 0.5em;
|
||||
gap: 0.5em;
|
||||
font: {
|
||||
family: "Courier", monospace;
|
||||
}
|
||||
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
$color-primary,
|
||||
$color-primary 20px,
|
||||
$color-background 20px,
|
||||
$color-background 40px
|
||||
);
|
||||
|
||||
.header-padding {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.App-header-item {
|
||||
font: {
|
||||
family: "Courier", monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.App-header {
|
||||
font: {
|
||||
size: calc(10px + 2vmin);
|
||||
weight: bold;
|
||||
}
|
||||
margin: auto;
|
||||
color: $color-background;
|
||||
background-color: $color-primary;
|
||||
padding: 0.25em;
|
||||
text-align: left;
|
||||
//border-radius: 4px;
|
||||
//box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2),
|
||||
// 0px 4px 5px 0px rgba(0,0,0,0.14),
|
||||
// 0px 1px 10px 0px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.App-link {
|
||||
|
|
|
@ -18,3 +18,9 @@ code {
|
|||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
//@media (max-width: $breakpoint-mobile){
|
||||
// .MuiBox-root {
|
||||
// padding: 6px !important;
|
||||
// }
|
||||
//}
|
|
@ -4,6 +4,10 @@
|
|||
align-items: flex-start;
|
||||
gap: 2em;
|
||||
|
||||
@media (max-width: $breakpoint-mobile) {
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
&>div {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
@ -39,7 +43,9 @@
|
|||
gap: 2em;
|
||||
align-items: flex-start;
|
||||
|
||||
|
||||
@media (max-width: $breakpoint-mobile){
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.Input {
|
||||
flex-basis: 70%;
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
|
||||
.ModePanel {
|
||||
max-width: 80%;
|
||||
//max-width: 80%;
|
||||
margin: auto;
|
||||
font: {
|
||||
family: "Courier", monospace;
|
||||
}
|
||||
|
||||
.TabsList {
|
||||
min-width: 400px;
|
||||
|
@ -19,12 +22,24 @@
|
|||
justify-content: center;
|
||||
align-content: space-between;
|
||||
|
||||
|
||||
|
||||
.Tab.Mui-selected {
|
||||
background-color: $color-primary;
|
||||
color: $color-background;
|
||||
}
|
||||
|
||||
}
|
||||
.Tab {
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
//font-size: 1000px;
|
||||
font: {
|
||||
family: 'Courier', monospace;
|
||||
size: 1.2em;
|
||||
}
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
//font-size: 1.5rem;
|
||||
//font-weight: bold;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
line-height: 1.5;
|
||||
|
@ -35,11 +50,12 @@
|
|||
justify-content: center;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.Tab.Mui-selected {
|
||||
background-color: $color-primary;
|
||||
color: $color-background;
|
||||
}
|
||||
.TabPanel {
|
||||
padding: 24px;
|
||||
|
||||
@media (max-width: $breakpoint-mobile){
|
||||
padding: 12px 12px;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
p a {
|
||||
background-color: $color-primary;
|
||||
color: $color-background;
|
||||
padding: 0.2em;
|
||||
font: {
|
||||
family: "Courier", monospace;
|
||||
}
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
code {
|
||||
font: {
|
||||
family: 'Courier', 'Courier New', monospace;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
$color-primary: #DED03A;
|
||||
$color-primary: #ffeb3b;
|
||||
$color-primary-darker: scale-color($color-primary, $lightness: -10%);
|
||||
$color-primary-background: adjust-color($color-primary, $alpha: -0.9, $saturation: -50%);
|
||||
$color-primary-border: adjust-color($color-primary, $alpha: -0.9, $lightness: 50%, $saturation: -50%);
|
||||
$color-secondary: #A167A5;
|
||||
$color-background: #1C1B22;
|
||||
$color-text: white;
|
||||
|
||||
$breakpoint-mobile: 600px;
|
10
client/src/types.d.ts
vendored
10
client/src/types.d.ts
vendored
|
@ -3,3 +3,13 @@ declare module "*.svg" {
|
|||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
// declare module '@mui/material/styles' {
|
||||
// interface Palette {
|
||||
// paleYellow: Palette['primary'];
|
||||
// }
|
||||
//
|
||||
// interface PaletteOptions {
|
||||
// paleYellow?: PaletteOptions['primary'];
|
||||
// }
|
||||
// }
|
|
@ -15,7 +15,7 @@ const scopes = ['bot', 'channels:write', 'channels:write.invites', 'chat:write:b
|
|||
const slackBridgeRepository = AppDataSource.getRepository(SlackBridge)
|
||||
const groupRepository = AppDataSource.getRepository(Group)
|
||||
|
||||
const SLACK_COOKIE_NAME = "slack-oauth-state";
|
||||
// const SLACK_COOKIE_NAME = "slack-oauth-state";
|
||||
|
||||
const slackConfig = config.get<slackConfigType>('slackConfig');
|
||||
|
||||
|
@ -45,7 +45,7 @@ export const SlackInstallLinkHandler = async(
|
|||
state_token
|
||||
);
|
||||
|
||||
res.cookie(SLACK_COOKIE_NAME, state_token, { maxAge: 60*5 })
|
||||
// res.cookie(SLACK_COOKIE_NAME, state_token, { maxAge: 60*5 })
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
data: {
|
||||
|
@ -78,7 +78,9 @@ export const SlackCallbackHandler = async(
|
|||
|
||||
// check if we have an entity
|
||||
let bridge = await slackBridgeRepository.findOneBy({Token: installation.bot.token})
|
||||
logger.debug('found existing bridge %s', bridge)
|
||||
if (!bridge){
|
||||
logger.debug('creating new bridge')
|
||||
bridge = await slackBridgeRepository.create(bridge_data);
|
||||
await slackBridgeRepository.save(bridge);
|
||||
logger.debug('created bridge')
|
||||
|
|
|
@ -19,7 +19,7 @@ export const cookieMiddleware = cookieSession({
|
|||
name: 'session',
|
||||
keys: [cookieConfig.key1, cookieConfig.key2],
|
||||
signed: true,
|
||||
|
||||
maxAge: 24*60*60*1000 // 24 hours
|
||||
})
|
||||
|
||||
export const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
|
||||
|
|
Loading…
Reference in a new issue