mirror of https://github.com/tycrek/ass
Merge branch 'patch/docker' of https://github.com/Gauvino/ass into patch/docker
commit
a8fda8800d
@ -0,0 +1,46 @@
|
||||
import { EndpointRateLimitConfiguration } from 'ass';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
|
||||
/**
|
||||
* map that contains rate limiter middleware for each group
|
||||
*/
|
||||
const rateLimiterGroups = new Map<string, (req: Request, res: Response, next: NextFunction) => void>();
|
||||
|
||||
export const setRateLimiter = (group: string, config: EndpointRateLimitConfiguration | undefined): (req: Request, res: Response, next: NextFunction) => void => {
|
||||
if (config == null) { // config might be null if the user doesnt want a rate limit
|
||||
rateLimiterGroups.set(group, (req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
return rateLimiterGroups.get(group)!;
|
||||
} else {
|
||||
rateLimiterGroups.set(group, rateLimit({
|
||||
limit: config.requests,
|
||||
windowMs: config.duration * 1000,
|
||||
skipFailedRequests: true,
|
||||
legacyHeaders: false,
|
||||
standardHeaders: 'draft-7',
|
||||
keyGenerator: (req, res) => {
|
||||
return req.ip || 'disconnected';
|
||||
},
|
||||
handler: (req, res) => {
|
||||
res.status(429);
|
||||
res.contentType('json');
|
||||
res.send('{"success":false,"message":"Rate limit exceeded, try again later"}');
|
||||
}
|
||||
}));
|
||||
|
||||
return rateLimiterGroups.get(group)!;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* creates middleware for rate limiting
|
||||
*/
|
||||
export const rateLimiterMiddleware = (group: string, config: EndpointRateLimitConfiguration | undefined): (req: Request, res: Response, next: NextFunction) => void => {
|
||||
if (!rateLimiterGroups.has(group)) setRateLimiter(group, config);
|
||||
|
||||
return (req, res, next) => {
|
||||
return rateLimiterGroups.get(group)!(req, res, next);
|
||||
};
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
import { Router } from 'express';
|
||||
import { path } from '@tycrek/joint';
|
||||
|
||||
import { App } from '../app';
|
||||
import { UserConfig } from '../UserConfig';
|
||||
|
||||
/**
|
||||
* Builds a basic router for loading a page with frontend JS
|
||||
*/
|
||||
export const buildFrontendRouter = (page: string, onConfigReady = true) => {
|
||||
|
||||
// Config readiness checker
|
||||
const ready = () => (onConfigReady)
|
||||
? UserConfig.ready
|
||||
: !UserConfig.ready;
|
||||
|
||||
// Set up a router
|
||||
const router = Router({ caseSensitive: true });
|
||||
|
||||
// Render the page
|
||||
router.get('/', (_req, res) => ready()
|
||||
? res.render(page, { version: App.pkgVersion })
|
||||
: res.redirect('/'));
|
||||
|
||||
// Load frontend JS
|
||||
router.get('/ui.js', (_req, res) => ready()
|
||||
? res.type('text/javascript').sendFile(path.join(`dist-frontend/${page}.mjs`))
|
||||
: res.sendStatus(403));
|
||||
|
||||
return router;
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
import { path } from '@tycrek/joint';
|
||||
import { Router, json as BodyParserJson } from 'express';
|
||||
import { log } from '../log';
|
||||
import { UserConfig } from '../UserConfig';
|
||||
import { setDataModeToSql } from '../data';
|
||||
import { MySql } from '../sql/mysql';
|
||||
import { App } from '../app';
|
||||
|
||||
const router = Router({ caseSensitive: true });
|
||||
|
||||
// Static routes
|
||||
router.get('/', (req, res) => UserConfig.ready ? res.redirect('/') : res.render('setup', { version: App.pkgVersion }));
|
||||
router.get('/ui.js', (req, res) => UserConfig.ready ? res.send('') : res.type('text/javascript').sendFile(path.join('dist-frontend/setup.mjs')));
|
||||
|
||||
// Setup route
|
||||
router.post('/', BodyParserJson(), async (req, res) => {
|
||||
if (UserConfig.ready)
|
||||
return res.status(409).json({ success: false, message: 'User config already exists' });
|
||||
|
||||
log.debug('Setup initiated');
|
||||
|
||||
try {
|
||||
// Parse body
|
||||
new UserConfig(req.body);
|
||||
|
||||
// Save config
|
||||
await UserConfig.saveConfigFile();
|
||||
|
||||
// Set data storage (not files) to SQL if required
|
||||
if (UserConfig.config.sql?.mySql != null)
|
||||
await Promise.all([MySql.configure(), setDataModeToSql()]);
|
||||
|
||||
return res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
return res.status(400).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
export { router };
|
@ -0,0 +1,4 @@
|
||||
import { SlInput, SlButton } from '@shoelace-style/shoelace';
|
||||
|
||||
// * Wait for the document to be ready
|
||||
document.addEventListener('DOMContentLoaded', () => console.log('Admin page loaded'));
|
@ -0,0 +1,46 @@
|
||||
import { SlInput, SlButton } from '@shoelace-style/shoelace';
|
||||
|
||||
const genericErrorAlert = () => alert('An error occured, please check the console for details');
|
||||
const errAlert = (logTitle: string, err: any, stream: 'error' | 'warn' = 'error') => (console[stream](logTitle, err), genericErrorAlert());
|
||||
const errReset = (message: string, element: SlButton) => (element.disabled = false, alert(message));
|
||||
|
||||
// * Wait for the document to be ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const Elements = {
|
||||
usernameInput: document.querySelector('#login-username') as SlInput,
|
||||
passwordInput: document.querySelector('#login-password') as SlInput,
|
||||
submitButton: document.querySelector('#login-submit') as SlButton
|
||||
};
|
||||
|
||||
// * Login button click handler
|
||||
Elements.submitButton.addEventListener('click', async () => {
|
||||
Elements.submitButton.disabled = true;
|
||||
|
||||
// Make sure fields are filled
|
||||
if (Elements.usernameInput.value == null || Elements.usernameInput.value === '')
|
||||
return errReset('Username is required!', Elements.submitButton);
|
||||
if (Elements.passwordInput.value == null || Elements.passwordInput.value === '')
|
||||
return errReset('Password is required!', Elements.submitButton);
|
||||
|
||||
fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: Elements.usernameInput.value,
|
||||
password: Elements.passwordInput.value
|
||||
})
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data: {
|
||||
success: boolean,
|
||||
message: string,
|
||||
meta: { redirectTo: string }
|
||||
}) => {
|
||||
if (!data.success) alert(data.message);
|
||||
else window.location.href = data.meta.redirectTo;
|
||||
})
|
||||
.catch((err) => errAlert('POST to /api/login failed!', err))
|
||||
.finally(() => Elements.submitButton.disabled = false);
|
||||
});
|
||||
});
|
@ -0,0 +1,4 @@
|
||||
import { SlInput, SlButton } from '@shoelace-style/shoelace';
|
||||
|
||||
// * Wait for the document to be ready
|
||||
document.addEventListener('DOMContentLoaded', () => console.log('User page loaded'));
|
@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Installing ass-docker for Linux..."
|
||||
|
||||
# Ensure that ./uploads/thumbnails/ exists
|
||||
mkdir -p ./uploads/thumbnails/
|
||||
|
||||
# Ensure that ./share/ exists
|
||||
mkdir -p ./share/
|
||||
|
||||
# Ensure that files config.json, auth.json, & data.json exist
|
||||
for value in config.json auth.json data.json
|
||||
do
|
||||
if [ ! -f $value ]; then
|
||||
touch $value
|
||||
fi
|
||||
done
|
||||
|
||||
# Wait for user to confirm
|
||||
echo "Continuing will run docker compose. Continue? (Press Ctrl+C to abort)"
|
||||
read -n 1 -s -r -p "Press any key to continue..."
|
||||
|
||||
echo Running setup...
|
||||
|
||||
# Bring up the container and run the setup
|
||||
docker compose up -d && docker compose exec ass npm run setup && docker compose restart
|
||||
|
||||
# Done!
|
||||
echo "ass-docker for Linux installed!"
|
||||
echo "Run the following to view commands:"
|
||||
echo "$ docker compose logs -f --tail=50 --no-log-prefix ass"
|
@ -1,28 +0,0 @@
|
||||
@echo off
|
||||
|
||||
ECHO Installing ass-docker for Windows...
|
||||
|
||||
REM Ensure that ./uploads/thumbnails/ exists
|
||||
if not exist "./uploads/thumbnails/" md "./uploads/thumbnails/"
|
||||
|
||||
REM Ensure that ./share/ exists
|
||||
if not exist "./share/" md "./share/"
|
||||
|
||||
REM Ensure that files config.json, auth.json, & data.json exist
|
||||
if not exist "./config.json" echo. >> "./config.json"
|
||||
if not exist "./auth.json" echo. >> "./auth.json"
|
||||
if not exist "./data.json" echo. >> "./data.json"
|
||||
|
||||
REM Wait for user to confirm
|
||||
ECHO Continuing will run docker compose. Continue? (Press Ctrl+C to abort)
|
||||
PAUSE
|
||||
|
||||
ECHO Running setup...
|
||||
|
||||
REM Bring up the container and run the setup
|
||||
docker compose up -d && docker compose exec ass npm run setup && docker compose restart
|
||||
|
||||
REM Done!
|
||||
ECHO ass-docker for Windows installed!
|
||||
ECHO Run the following to view commands:
|
||||
ECHO > docker compose logs -f --tail=50 --no-log-prefix ass
|
@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Script Configuration
|
||||
# Load configuration file if available
|
||||
# this is useful if you want to source keys from a secret file
|
||||
CONFIG_FILE="config.sh"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# shellcheck disable=1090
|
||||
source "${CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
LOG_DIR=$(pwd)
|
||||
if [ ! -d "$LOG_DIR" ]; then
|
||||
echo "The directory you have specified to save the logs does not exist."
|
||||
echo "Please create the directory with the following command:"
|
||||
echo "mkdir -p $LOG_DIR"
|
||||
echo -en "Or specify a different LOG_DIR\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE_PATH="$HOME/Pictures"
|
||||
if [ ! -d "$IMAGE_PATH" ]; then
|
||||
echo "The directory you have specified to save the screenshot does not exist."
|
||||
echo "Please create the directory with the following command:"
|
||||
echo "mkdir -p $IMAGE_PATH"
|
||||
echo -en "Or specify a different IMAGE_PATH\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE_NAME="ass"
|
||||
FILE="${IMAGE_PATH}/${IMAGE_NAME}.png"
|
||||
|
||||
# Function to check if a tool is installed
|
||||
check_tool() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to take Flameshot screenshots
|
||||
takeFlameshot() {
|
||||
# check if flameshot tool is installed
|
||||
REQUIRED_TOOLS=("flameshot")
|
||||
|
||||
for tool in "${REQUIRED_TOOLS[@]}"; do
|
||||
if ! check_tool "$tool"; then
|
||||
echo "Error: $tool is not installed. Please install it before using this script."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
flameshot config -f "${IMAGE_NAME}"
|
||||
flameshot gui -r -p "${IMAGE_PATH}" >/dev/null
|
||||
}
|
||||
|
||||
# Function to take Wayland screenshots using grim + slurp
|
||||
takeGrimshot() {
|
||||
# check if grim and slurp are installed
|
||||
REQUIRED_TOOLS=("grim" "slurp")
|
||||
for tool in "${REQUIRED_TOOLS[@]}"; do
|
||||
if ! check_tool "$tool"; then
|
||||
echo "Error: $tool is not installed. Please install it before using this script."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
grim -g "$(slurp)" "${FILE}" >/dev/null
|
||||
}
|
||||
|
||||
# Function to remove the taken screenshot
|
||||
removeTargetFile() {
|
||||
echo -en "Process complete.\nRemoving image.\n"
|
||||
rm -v "${FILE}"
|
||||
}
|
||||
|
||||
# Function to upload target image to your ass instance
|
||||
uploadScreenshot() {
|
||||
echo -en "KEY & DOMAIN are set. Attempting to upload to your ass instance.\n"
|
||||
URL=$(curl -X POST \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-H "Accept: application/json" \
|
||||
-H "User-Agent: ShareX/13.4.0" \
|
||||
-H "Authorization: $KEY" \
|
||||
-F "file=@${FILE}" "https://$DOMAIN/" | grep -Po '(?<="resource":")[^"]+')
|
||||
if [[ "${XDG_SESSION_TYPE}" == x11 ]]; then
|
||||
printf "%s" "$URL" | xclip -sel clip
|
||||
elif [[ "${XDG_SESSION_TYPE}" == wayland ]]; then
|
||||
printf "%s" "$URL" | wl-copy
|
||||
else
|
||||
echo -en "Invalid desktop session!\nExiting.\n"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
localScreenshot() {
|
||||
echo -en "KEY & DOMAIN variables are not set. Attempting local screenshot.\n"
|
||||
if [[ "${XDG_SESSION_TYPE}" == x11 ]]; then
|
||||
xclip -sel clip -target image/png <"${FILE}"
|
||||
elif [[ "${XDG_SESSION_TYPE}" == wayland ]]; then
|
||||
wl-copy <"${FILE}"
|
||||
else
|
||||
echo -en "Unknown display backend. Assuming Xorg and using xclip.\n"
|
||||
xclip -sel clip -target image/png <"${FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if the screenshot tool based on display backend
|
||||
if [[ "${XDG_SESSION_TYPE}" == x11 ]]; then
|
||||
echo -en "Display backend detected as Xorg (x11), using Flameshot\n"
|
||||
takeFlameshot
|
||||
elif [[ "${XDG_SESSION_TYPE}" == wayland ]]; then
|
||||
echo -en "Display backend detected as Wayland, using grim & slurp\n"
|
||||
takeGrimshot
|
||||
else
|
||||
echo -en "Unknown display backend. Assuming Xorg and using Flameshot\n"
|
||||
takeFlameshot >"${LOG_DIR}/flameshot.log"
|
||||
echo -en "Done. Make sure you check for any errors and report them.\nLogfile located in '${LOG_DIR}'\n"
|
||||
fi
|
||||
|
||||
# Check if the screenshot file exists before proceeding
|
||||
if [[ -f "${FILE}" ]]; then
|
||||
if [[ -n "$KEY" && -n "$DOMAIN" ]]; then
|
||||
# Upload the file to the ass instance
|
||||
uploadImage
|
||||
|
||||
# Remove image
|
||||
removeTargetFile
|
||||
else
|
||||
# Take a screenshot locally
|
||||
localScreenshot
|
||||
|
||||
# Remove image
|
||||
removeTargetFile
|
||||
fi
|
||||
else
|
||||
echo -en "Target file ${FILE} was not found. Aborting screenshot.\n"
|
||||
exit 1
|
||||
fi
|
@ -1,33 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {}
|
||||
|
||||
@layer components {
|
||||
.res-media {
|
||||
@apply border-l-4 rounded max-h-half-port;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply no-underline hover_no-underline active_no-underline visited_no-underline
|
||||
|
||||
/* regular, visited */
|
||||
text-link-primary visited_text-link-primary
|
||||
border-b-2 visited_border-b-2
|
||||
border-transparent visited_border-transparent
|
||||
rounded-sm visited_rounded-sm
|
||||
|
||||
/* hover */
|
||||
hover_text-link-hover
|
||||
hover_border-hover
|
||||
|
||||
/* active */
|
||||
active_text-link-active
|
||||
|
||||
/* transitions */
|
||||
ease-linear duration-150 transition-all;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {}
|
@ -0,0 +1,9 @@
|
||||
extends _base_
|
||||
block title
|
||||
title ass user 🍑
|
||||
block section
|
||||
span user
|
||||
block content
|
||||
h1.text-3xl Coming soon.
|
||||
|
||||
script(src='/user/ui.js')
|
Loading…
Reference in new issue