Compare commits

..

No commits in common. "main" and "v0.0.1" have entirely different histories.
main ... v0.0.1

8 changed files with 811 additions and 855 deletions

View File

@ -1,4 +1,4 @@
FROM node:alpine as builder FROM node:alpine
EXPOSE 3000 EXPOSE 3000
WORKDIR /app WORKDIR /app
@ -9,13 +9,9 @@ COPY tsconfig.json .
RUN npm install RUN npm install
COPY --chmod=111 startup.sh .
COPY public public COPY public public
COPY src src COPY src src
RUN npm run build ENTRYPOINT [ "/usr/bin/env", "./startup.sh" ]
FROM node:alpine
COPY --from=builder /app/build /opt/server
WORKDIR /opt/server
ENTRYPOINT [ "npx", "-y" , "serve", "-s", "/opt/server" ]

View File

@ -1 +0,0 @@
var API_URL = 'https://games.acooldomain.co'

801
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
var API_URL = "http://localhost"

View File

@ -3,7 +3,6 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.svg"/> <link rel="icon" href="%PUBLIC_URL%/favicon.svg"/>
<script type="text/javascript" src="%PUBLIC_URL%/config.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta

View File

@ -1,7 +1,7 @@
import axios, { AxiosInstance, AxiosResponse } from "axios"; import axios, { AxiosInstance, AxiosResponse } from "axios";
import React, { Context, Dispatch, ReactNode, createContext, useContext, useState } from "react"; import React, { Context, Dispatch, ReactNode, createContext, useContext, useState } from "react";
import { useLocation, Navigate } from "react-router-dom"; import { useLocation, Navigate } from "react-router-dom";
import Cookies from 'js-cookie'; import Cookies from 'js-cookie'
import { Box, Button, ButtonGroup, ButtonOwnProps, Modal, PaletteMode, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material"; import { Box, Button, ButtonGroup, ButtonOwnProps, Modal, PaletteMode, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from "@mui/material";
import { Form } from "@rjsf/mui"; import { Form } from "@rjsf/mui";
import validator from '@rjsf/validator-ajv8'; import validator from '@rjsf/validator-ajv8';
@ -19,7 +19,7 @@ import { Permission } from "./actions";
import TerminalComponent from "./terminal"; import TerminalComponent from "./terminal";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
export const apiAuthenticatedContext: Context<[boolean, Dispatch<boolean>]> = createContext([false, (value: boolean) => { }] as [boolean, Dispatch<boolean>]) export const apiAuthenticatedContext: Context<[boolean, Dispatch<boolean>]> = createContext([false, (value: boolean) => {}] as [boolean, Dispatch<boolean>])
export const formModalStyle = { export const formModalStyle = {
@ -86,16 +86,17 @@ export const getDesignTokens = (mode: PaletteMode) => ({
} }
}); });
const API_URL = (window as any).API_URL const API_URL = `${process.env.REACT_APP_API_SCHEME}://${process.env.REACT_APP_API_URL}`
axios.defaults.withCredentials = true axios.defaults.withCredentials = true
export const api: AxiosInstance = axios.create({ export const api: AxiosInstance = axios.create({
baseURL: API_URL, baseURL: API_URL,
withCredentials: true, withCredentials: true,
}); });
export function ApiWrapper(p: { children: ReactNode }) { export function ApiWrapper(p: { children: ReactNode}) {
const { children } = p const {children} = p
const token = Cookies.get('auth') const token = Cookies.get('auth')
const [apiAuthenticated, setApiAuthenticated] = useState(Boolean(token)) const [apiAuthenticated, setApiAuthenticated] = useState(Boolean(token))
@ -120,20 +121,20 @@ export interface ActionInfo {
} }
interface Options { interface Options{
label: string label: string
const: string const: string
} }
export const actionIdentifierContext: Context<string> = createContext('') export const actionIdentifierContext: Context<string> = createContext('')
function convertNumber(permissions: number): number[] { function convertNumber(permissions: number): number[]{
var arr: number[] = [] var arr: number[] = []
Object.entries(Permission).forEach( Object.entries(Permission).forEach(
([key, value]) => { ([key, value]) => {
if (permissions & value) { if (permissions&value){
arr.push(value) arr.push(value)
} }
} }
@ -142,42 +143,41 @@ function convertNumber(permissions: number): number[] {
return arr return arr
} }
function CustomField(props: WidgetProps) { function CustomField(props: WidgetProps){
const jp = require('jsonpath') const jp = require('jsonpath')
const [options2, setOptions]: [Options[] | null, Dispatch<Options[]>] = useState(null as Options[] | null) const [options2, setOptions]: [Options[]|null, Dispatch<Options[]>] = useState(null as Options[]|null)
const { schema, registry, options, ...newProps } = props const {schema, registry, options, ...newProps} = props
const { SelectWidget, CheckboxesWidget } = registry.widgets const {SelectWidget, CheckboxesWidget} = registry.widgets
if (!schema.fetch_url) { if (!schema.fetch_url){
if (!schema.permissions) { if (!schema.permissions){
return <TextField onChange={(event) => (props.onChange(event.target.value))} value={props.value} label={props.label} /> return <TextField onChange={(event)=>(props.onChange(event.target.value))} value={props.value} label={props.label}/>
} }
return <CheckboxesWidget return <CheckboxesWidget
{...newProps} {...newProps}
onChange={(event) => { onChange={(event)=>{
props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0)) props.onChange(event.reduce((partialSum: number, a: number) => (partialSum + a), 0))
} }
} }
schema={{}} schema={{}}
options={{ options={{
enumOptions: [ enumOptions: [
{ label: 'Start', value: Permission.Start }, {label: 'Start', value: Permission.Start},
{ label: 'Stop', value: Permission.Stop }, {label: 'Stop', value: Permission.Stop},
{ label: 'Browse', value: Permission.Browse }, {label: 'Browse', value: Permission.Browse},
{ label: 'Delete', value: Permission.Delete }, {label: 'Delete', value: Permission.Delete},
{ label: 'Run Command', value: Permission.RunCommand }, {label: 'Run Command', value: Permission.RunCommand},
{ label: 'Create', value: Permission.Create }, {label: 'Create', value: Permission.Create},
{ label: 'Admin', value: Permission.Admin }, {label: 'Admin', value: Permission.Admin},
{ label: 'Cloud', value: Permission.Cloud }, {label: 'Cloud', value: Permission.Cloud},
] ]}} registry={registry} value={convertNumber(props.value)} />
}} registry={registry} value={convertNumber(props.value)} />
} }
if (options2 === null) { if (options2 === null){
api.get(schema.fetch_url as string).then((event) => { api.get(schema.fetch_url as string).then((event)=>{
let newOptions: Options[] = [] let newOptions: Options[] = []
for (let response of event.data) { for (let response of event.data){
newOptions.push({ newOptions.push({
const: jp.query(response, `$.${schema.fetch_key_path}`).join(' '), const: jp.query(response, `$.${schema.fetch_key_path}`).join(' '),
label: jp.query(response, `$.${schema.fetch_display_path}`).join(' '), label: jp.query(response, `$.${schema.fetch_display_path}`).join(' '),
@ -187,25 +187,25 @@ function CustomField(props: WidgetProps) {
setOptions(newOptions) setOptions(newOptions)
}) })
} }
return <SelectWidget {...newProps} schema={{ oneOf: options2 ? options2 : [] }} registry={registry} options={{ enumOptions: (options2 ? options2 : []).map((value: Options) => ({ label: value.label, value: value.const })) }} /> return <SelectWidget {...newProps} schema={{oneOf: options2?options2:[]}} registry={registry} options={{enumOptions: (options2?options2:[]).map((value: Options)=>({label: value.label, value: value.const}))}}/>
} }
function isUserAllowed(user: User | null, action: ActionInfo): boolean { function isUserAllowed(user: User|null, action: ActionInfo): boolean{
if (user === null) { if (user === null){
return false return false
} }
const isAdmin = (user.Permissions & Permission.Admin) === Permission.Admin const isAdmin = (user.Permissions & Permission.Admin) === Permission.Admin
if (isAdmin) { if (isAdmin){
return true return true
} }
if (!action.permissions) { if (!action.permissions){
return true return true
} }
if ((action.permissions & user.Permissions) == action.permissions) { if ((action.permissions & user.Permissions) == action.permissions){
return true return true
} }
@ -219,41 +219,41 @@ export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string
const user = useContext(UserInfoContext) const user = useContext(UserInfoContext)
const [form, setForm] = useState(false); const [form, setForm] = useState(false);
const [terminal, setTerminal] = useState(null as string | null); const [terminal, setTerminal] = useState(null as string|null);
const [formData, setFormData]: [RJSFSchema, Dispatch<RJSFSchema>] = useState({}) const [formData, setFormData]: [RJSFSchema, Dispatch<RJSFSchema>] = useState({})
const url = p.action.endpoint.replaceAll(`{${identifierSubstring}}`, actionIdentifier) const url = p.action.endpoint.replaceAll(`{${identifierSubstring}}`, actionIdentifier)
function handleSubmit() { function handleSubmit() {
let promise: Promise<AxiosResponse<any, any>> | null = null let promise: Promise<AxiosResponse<any, any>>|null = null
switch (p.action.requestType) { switch (p.action.requestType) {
case 'post': { case 'post': {
promise = api.post(url, formData) promise = api.post(url, formData)
break break
} }
case 'patch': { case 'patch':{
promise = api.patch(url, formData) promise = api.patch(url, formData)
break break
} }
case 'get': { case 'get': {
if (formData) { if (formData){
console.warn('get can get no arguments, dropping') console.warn('get can get no arguments, dropping')
} }
promise = api.get(url) promise = api.get(url)
break break
} }
case 'delete': { case 'delete': {
if (formData) { if (formData){
console.warn('delete can get no arguments, dropping') console.warn('delete can get no arguments, dropping')
} }
promise = api.delete(url) promise = api.delete(url)
break break
} }
} }
switch (p.action.response_action) { switch (p.action.response_action){
case 'Browse': { case 'Browse':{
if (promise !== null) { if (promise !== null){
promise.then((event) => { window.open(`https://${event.data}`) }) promise.then((event)=>{window.open(`https://${event.data}`)})
} }
} }
} }
@ -265,7 +265,7 @@ export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string
setFormData(args.formData) setFormData(args.formData)
} }
function createTerminal(websocket: string) { function createTerminal(websocket: string){
const NewWindow = window.open('', '', 'width=800 height=600')! const NewWindow = window.open('', '', 'width=800 height=600')!
NewWindow.document.write(`<!doctype html> NewWindow.document.write(`<!doctype html>
<html> <html>
@ -331,18 +331,18 @@ export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string
`) `)
NewWindow.onload = () => { NewWindow.onload = () => {
const root = ReactDOM.createRoot(NewWindow.document.getElementById('root') as HTMLElement); const root = ReactDOM.createRoot(NewWindow.document.getElementById('root') as HTMLElement);
root.render(<TerminalComponent websocket={websocket} />); root.render(<TerminalComponent websocket={websocket}/>);
}; };
} }
return (<> return (<>
<Button variant={p.variant} disabled={!isUserAllowed(user, p.action)} onClick={() => { if (p.onClick) { p.onClick() } p.action.response_action == 'Terminal' ? createTerminal(`ws${API_URL.slice("http".length)}${url}`) : setForm(true) }} sx={p.sx}>{p.action.name}</Button > <Button variant={p.variant} disabled={!isUserAllowed(user, p.action)} onClick={() => { if (p.onClick) { p.onClick() } p.action.response_action == 'Terminal'?createTerminal(`ws${API_URL.slice("http".length)}${url}`):setForm(true) }} sx={p.sx}>{p.action.name}</Button >
<Modal <Modal
onClose={() => { setForm(false); setFormData({}); }} onClose={() => { setForm(false); setFormData({}); }}
open={form} open={form}
> >
<Box sx={{ ...formModalStyle }}> <Box sx={{ ...formModalStyle}}>
<Form validator={validator} widgets={{ TextWidget: CustomField }} schema={p.action.args} onChange={onFormChange} formData={formData} onSubmit={handleSubmit} /> <Form validator={validator} widgets={{TextWidget: CustomField}} schema={p.action.args} onChange={onFormChange} formData={formData} onSubmit={handleSubmit} />
</Box> </Box>
</Modal> </Modal>
<Modal <Modal
@ -356,14 +356,14 @@ export function ActionItem(p: { action: ActionInfo, identifierSubstring?: string
</>) </>)
} }
export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: string, children?: ReactNode }) { export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: string, children?: ReactNode}) {
const actionItems: any[] = p.actions.map((action, index, array) => (<ActionItem action={action} identifierSubstring={p.identifierSubstring} />)) const actionItems: any[] = p.actions.map((action, index, array) => (<ActionItem action={action} identifierSubstring={p.identifierSubstring} /> ))
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef<HTMLDivElement>(null); const anchorRef = React.useRef<HTMLDivElement>(null);
const [selectedIndex, setSelectedIndex] = React.useState(0); const [selectedIndex, setSelectedIndex] = React.useState(0);
const user = useContext(UserInfoContext) const user = useContext(UserInfoContext)
for (let child of React.Children.toArray(p.children)) { for (let child of React.Children.toArray(p.children)){
actionItems.push(child) actionItems.push(child)
} }
@ -451,7 +451,7 @@ export function ActionGroup(p: { actions: ActionInfo[], identifierSubstring?: st
export function DataTable(props: { headers: string[], children: ReactNode, actionInfo?: ActionInfo, actionHook?: Function }) { export function DataTable(props: { headers: string[], children: ReactNode, actionInfo?: ActionInfo, actionHook?: Function }) {
const { children, headers, actionInfo, actionHook } = props const { children, headers, actionInfo, actionHook } = props
return <Box padding={4} overflow='clip'> return <Box padding={4} overflow='clip'>
<TableContainer component={Paper} sx={{ maxHeight: '80svh' }}> <TableContainer component={Paper} sx={{maxHeight: '80svh'}}>
<Table stickyHeader> <Table stickyHeader>
<TableHead> <TableHead>
<TableRow sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}> <TableRow sx={{ backgroundColor: 'background.light', fontWeight: 'bold' }}>
@ -470,17 +470,17 @@ export function DataTable(props: { headers: string[], children: ReactNode, actio
} }
export const UserInfoContext: Context<User | null> = createContext(null as User | null) export const UserInfoContext: Context<User|null> = createContext(null as User|null)
export function GlobalUserInfo(props: { children: any }) { export function GlobalUserInfo(props: {children: any}){
const [user, setUser]: [User | null, Dispatch<User | null>] = useState(null as User | null) const [user, setUser]: [User|null, Dispatch<User|null>] = useState(null as User|null)
const [apiAuthenticated, _] = useContext(apiAuthenticatedContext) const [apiAuthenticated, _] = useContext(apiAuthenticatedContext)
if (user === null && apiAuthenticated) { if (user === null && apiAuthenticated){
api.get('/users/@me').then((event) => { setUser(event.data) }).catch(() => { setUser(null) }) api.get('/users/@me').then((event)=>{setUser(event.data)}).catch(()=>{setUser(null)})
} }
return <UserInfoContext.Provider value={user}> return <UserInfoContext.Provider value={user}>

View File

@ -7,7 +7,7 @@ import { fetchToken } from "./login";
const signUp = async (username: string, password: string, token: string) => { const signUp = async (username: string, password: string, token: string) => {
try { try {
const response = await api.post(`/auth/signup?token=${token}`, { const response = await api.post(`/signup?token=${token}`, {
username: username, username: username,
password: password, password: password,
}, { }, {
@ -23,7 +23,7 @@ export function SignupPage(props: {}) {
const [apiAuthenticated, setApiAuthenticated] = useContext(apiAuthenticatedContext) const [apiAuthenticated, setApiAuthenticated] = useContext(apiAuthenticatedContext)
const [searchParam, setSearchParam] = useSearchParams(); const [searchParam, setSearchParam] = useSearchParams();
const token = searchParam.get('token'); const token = searchParam.get('token');
if (token === null) { if (token === null){
return <Navigate to='/' /> return <Navigate to='/' />
} }
@ -36,7 +36,7 @@ export function SignupPage(props: {}) {
const data = new FormData(event.currentTarget); const data = new FormData(event.currentTarget);
const usernameFormData: FormDataEntryValue | null = data.get('username'); const usernameFormData: FormDataEntryValue | null = data.get('username');
const passwordFormData: FormDataEntryValue | null = data.get('password'); const passwordFormData: FormDataEntryValue | null = data.get('password');
if (usernameFormData === null || passwordFormData === null) { if (usernameFormData === null || passwordFormData === null){
return return
} }
const username: string = usernameFormData.toString(); const username: string = usernameFormData.toString();