mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-03-06 10:55:56 +01:00
Compare commits
4 Commits
2026-03-02
...
tinyauth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d7cec2a3 | ||
|
|
f9b59d7634 | ||
|
|
f279536eb7 | ||
|
|
8f34c7cd2e |
@@ -412,6 +412,12 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
|||||||
|
|
||||||
## 2026-03-03
|
## 2026-03-03
|
||||||
|
|
||||||
|
### 🌐 Website
|
||||||
|
|
||||||
|
- #### 🐞 Bug Fixes
|
||||||
|
|
||||||
|
- Revert #11534 PR that messed up search [@BramSuurdje](https://github.com/BramSuurdje) ([#12492](https://github.com/community-scripts/ProxmoxVE/pull/12492))
|
||||||
|
|
||||||
## 2026-03-02
|
## 2026-03-02
|
||||||
|
|
||||||
### 🆕 New Scripts
|
### 🆕 New Scripts
|
||||||
|
|||||||
@@ -35,6 +35,20 @@ function update_script() {
|
|||||||
$STD service tinyauth stop
|
$STD service tinyauth stop
|
||||||
msg_ok "Service Stopped"
|
msg_ok "Service Stopped"
|
||||||
|
|
||||||
|
if [[ -f /opt/tinyauth/.env ]] && ! grep -q "^TINYAUTH_" /opt/tinyauth/.env; then
|
||||||
|
msg_info "Migrating .env to v5 format"
|
||||||
|
sed -i \
|
||||||
|
-e 's/^DATABASE_PATH=/TINYAUTH_DATABASE_PATH=/' \
|
||||||
|
-e 's/^USERS=/TINYAUTH_AUTH_USERS=/' \
|
||||||
|
-e "s/^USERS='/TINYAUTH_AUTH_USERS='/" \
|
||||||
|
-e 's/^APP_URL=/TINYAUTH_APPURL=/' \
|
||||||
|
-e 's/^SECRET=/TINYAUTH_AUTH_SECRET=/' \
|
||||||
|
-e 's/^PORT=/TINYAUTH_SERVER_PORT=/' \
|
||||||
|
-e 's/^ADDRESS=/TINYAUTH_SERVER_ADDRESS=/' \
|
||||||
|
/opt/tinyauth/.env
|
||||||
|
msg_ok "Migrated .env to v5 format"
|
||||||
|
fi
|
||||||
|
|
||||||
msg_info "Updating Tinyauth"
|
msg_info "Updating Tinyauth"
|
||||||
rm -f /opt/tinyauth/tinyauth
|
rm -f /opt/tinyauth/tinyauth
|
||||||
curl -fsSL "https://github.com/steveiliop56/tinyauth/releases/download/v${RELEASE}/tinyauth-amd64" -o /opt/tinyauth/tinyauth
|
curl -fsSL "https://github.com/steveiliop56/tinyauth/releases/download/v${RELEASE}/tinyauth-amd64" -o /opt/tinyauth/tinyauth
|
||||||
|
|||||||
53
ct/tinyauth.sh
Normal file
53
ct/tinyauth.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||||
|
# Copyright (c) 2021-2026 community-scripts ORG
|
||||||
|
# Author: MickLesk (CanbiZ)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://github.com/steveiliop56/tinyauth
|
||||||
|
|
||||||
|
APP="Tinyauth"
|
||||||
|
var_tags="${var_tags:-auth}"
|
||||||
|
var_cpu="${var_cpu:-1}"
|
||||||
|
var_ram="${var_ram:-512}"
|
||||||
|
var_disk="${var_disk:-4}"
|
||||||
|
var_os="${var_os:-debian}"
|
||||||
|
var_version="${var_version:-13}"
|
||||||
|
var_unprivileged="${var_unprivileged:-1}"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
if [[ ! -d /opt/tinyauth ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
if check_for_gh_release "tinyauth" "steveiliop56/tinyauth"; then
|
||||||
|
msg_info "Stopping Service"
|
||||||
|
systemctl stop tinyauth
|
||||||
|
msg_ok "Stopped Service"
|
||||||
|
|
||||||
|
fetch_and_deploy_gh_release "tinyauth" "steveiliop56/tinyauth" "singlefile" "latest" "/opt/tinyauth" "tinyauth-amd64"
|
||||||
|
|
||||||
|
msg_info "Starting Service"
|
||||||
|
systemctl start tinyauth
|
||||||
|
msg_ok "Started Service"
|
||||||
|
msg_ok "Updated successfully!"
|
||||||
|
fi
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Alpine-Tinyauth",
|
|
||||||
"slug": "alpine-tinyauth",
|
|
||||||
"categories": [
|
|
||||||
6
|
|
||||||
],
|
|
||||||
"date_created": "2025-05-06",
|
|
||||||
"type": "ct",
|
|
||||||
"updateable": true,
|
|
||||||
"privileged": false,
|
|
||||||
"interface_port": 3000,
|
|
||||||
"documentation": "https://tinyauth.app",
|
|
||||||
"config_path": "/opt/tinyauth/.env",
|
|
||||||
"website": "https://tinyauth.app",
|
|
||||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tinyauth.webp",
|
|
||||||
"description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps.",
|
|
||||||
"install_methods": [
|
|
||||||
{
|
|
||||||
"type": "default",
|
|
||||||
"script": "ct/alpine-tinyauth.sh",
|
|
||||||
"resources": {
|
|
||||||
"cpu": 1,
|
|
||||||
"ram": 256,
|
|
||||||
"hdd": 2,
|
|
||||||
"os": "alpine",
|
|
||||||
"version": "3.23"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "alpine",
|
|
||||||
"script": "ct/alpine-tinyauth.sh",
|
|
||||||
"resources": {
|
|
||||||
"cpu": 1,
|
|
||||||
"ram": 256,
|
|
||||||
"hdd": 2,
|
|
||||||
"os": "alpine",
|
|
||||||
"version": "3.23"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"default_credentials": {
|
|
||||||
"username": null,
|
|
||||||
"password": null
|
|
||||||
},
|
|
||||||
"notes": [
|
|
||||||
{
|
|
||||||
"text": "The default credentials are located in `/opt/tinyauth/credentials.txt`.",
|
|
||||||
"type": "info"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated": "2026-03-03T00:21:34Z",
|
"generated": "2026-03-03T06:17:56Z",
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
{
|
||||||
"slug": "2fauth",
|
"slug": "2fauth",
|
||||||
@@ -291,9 +291,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "dispatcharr",
|
"slug": "dispatcharr",
|
||||||
"repo": "Dispatcharr/Dispatcharr",
|
"repo": "Dispatcharr/Dispatcharr",
|
||||||
"version": "v0.20.1",
|
"version": "v0.20.2",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-26T21:38:19Z"
|
"date": "2026-03-03T01:40:33Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "docmost",
|
"slug": "docmost",
|
||||||
@@ -382,9 +382,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "firefly",
|
"slug": "firefly",
|
||||||
"repo": "firefly-iii/firefly-iii",
|
"repo": "firefly-iii/firefly-iii",
|
||||||
"version": "v6.5.1",
|
"version": "v6.5.2",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-27T20:55:55Z"
|
"date": "2026-03-03T05:42:27Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "fladder",
|
"slug": "fladder",
|
||||||
@@ -613,9 +613,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "jackett",
|
"slug": "jackett",
|
||||||
"repo": "Jackett/Jackett",
|
"repo": "Jackett/Jackett",
|
||||||
"version": "v0.24.1247",
|
"version": "v0.24.1261",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-03-02T05:56:37Z"
|
"date": "2026-03-03T05:54:20Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "jellystat",
|
"slug": "jellystat",
|
||||||
@@ -1733,9 +1733,9 @@
|
|||||||
{
|
{
|
||||||
"slug": "wealthfolio",
|
"slug": "wealthfolio",
|
||||||
"repo": "afadil/wealthfolio",
|
"repo": "afadil/wealthfolio",
|
||||||
"version": "v3.0.0",
|
"version": "v3.0.2",
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"date": "2026-02-24T22:37:05Z"
|
"date": "2026-03-03T05:01:49Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "web-check",
|
"slug": "web-check",
|
||||||
|
|||||||
51
frontend/public/json/tinyauth.json
Normal file
51
frontend/public/json/tinyauth.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "Tinyauth",
|
||||||
|
"slug": "tinyauth",
|
||||||
|
"categories": [
|
||||||
|
6
|
||||||
|
],
|
||||||
|
"date_created": "2025-05-06",
|
||||||
|
"type": "ct",
|
||||||
|
"updateable": true,
|
||||||
|
"privileged": false,
|
||||||
|
"interface_port": 3000,
|
||||||
|
"documentation": "https://tinyauth.app",
|
||||||
|
"config_path": "/opt/tinyauth/.env",
|
||||||
|
"website": "https://tinyauth.app",
|
||||||
|
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tinyauth.webp",
|
||||||
|
"description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps.",
|
||||||
|
"install_methods": [
|
||||||
|
{
|
||||||
|
"type": "default",
|
||||||
|
"script": "ct/tinyauth.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 1,
|
||||||
|
"ram": 512,
|
||||||
|
"hdd": 4,
|
||||||
|
"os": "debian",
|
||||||
|
"version": "13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "alpine",
|
||||||
|
"script": "ct/alpine-tinyauth.sh",
|
||||||
|
"resources": {
|
||||||
|
"cpu": 1,
|
||||||
|
"ram": 256,
|
||||||
|
"hdd": 2,
|
||||||
|
"os": "alpine",
|
||||||
|
"version": "3.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default_credentials": {
|
||||||
|
"username": null,
|
||||||
|
"password": null
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
{
|
||||||
|
"text": "The default credentials are located in `/opt/tinyauth/credentials.txt`.",
|
||||||
|
"type": "info"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,42 +2,68 @@
|
|||||||
|
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
import { CalendarIcon, Check, Clipboard, Download } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
import type { Category } from "@/lib/types";
|
import type { Category } from "@/lib/types";
|
||||||
|
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { basePath } from "@/config/site-config";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { fetchCategories } from "@/lib/data";
|
import { fetchCategories } from "@/lib/data";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import type { Script } from "./_schemas/schemas";
|
import type { Script } from "./_schemas/schemas";
|
||||||
|
|
||||||
|
import { ScriptItem } from "../scripts/_components/script-item";
|
||||||
import InstallMethod from "./_components/install-method";
|
import InstallMethod from "./_components/install-method";
|
||||||
import { ScriptSchema } from "./_schemas/schemas";
|
import { ScriptSchema } from "./_schemas/schemas";
|
||||||
import Categories from "./_components/categories";
|
import Categories from "./_components/categories";
|
||||||
import Note from "./_components/note";
|
import Note from "./_components/note";
|
||||||
|
|
||||||
import { githubGist, nord } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
function search(scripts: Script[], query: string): Script[] {
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
const queryLower = query.toLowerCase().trim();
|
||||||
import { ScriptItem } from "../scripts/_components/script-item";
|
const searchWords = queryLower.split(/\s+/).filter(Boolean);
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
return scripts
|
||||||
import { search } from "@/components/command-menu";
|
.map((script) => {
|
||||||
import { basePath } from "@/config/site-config";
|
const nameLower = script.name.toLowerCase();
|
||||||
import Image from "next/image";
|
const descriptionLower = (script.description || "").toLowerCase();
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
for (const word of searchWords) {
|
||||||
|
if (nameLower.includes(word)) {
|
||||||
|
score += 10;
|
||||||
|
}
|
||||||
|
if (descriptionLower.includes(word)) {
|
||||||
|
score += 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { script, score };
|
||||||
|
})
|
||||||
|
.filter(({ score }) => score > 0)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, 20)
|
||||||
|
.map(({ script }) => script);
|
||||||
|
}
|
||||||
|
|
||||||
const initialScript: Script = {
|
const initialScript: Script = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -77,32 +103,32 @@ export default function JSONGenerator() {
|
|||||||
|
|
||||||
const selectedCategoryObj = useMemo(
|
const selectedCategoryObj = useMemo(
|
||||||
() => categories.find(cat => cat.id.toString() === selectedCategory),
|
() => categories.find(cat => cat.id.toString() === selectedCategory),
|
||||||
[categories, selectedCategory]
|
[categories, selectedCategory],
|
||||||
);
|
);
|
||||||
|
|
||||||
const allScripts = useMemo(
|
const allScripts = useMemo(
|
||||||
() => categories.flatMap(cat => cat.scripts || []),
|
() => categories.flatMap(cat => cat.scripts || []),
|
||||||
[categories]
|
[categories],
|
||||||
);
|
);
|
||||||
|
|
||||||
const scripts = useMemo(() => {
|
const scripts = useMemo(() => {
|
||||||
const query = searchQuery.trim()
|
const query = searchQuery.trim();
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
return search(allScripts, query)
|
return search(allScripts, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCategoryObj) {
|
if (selectedCategoryObj) {
|
||||||
return selectedCategoryObj.scripts || []
|
return selectedCategoryObj.scripts || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return [];
|
||||||
}, [allScripts, selectedCategoryObj, searchQuery]);
|
}, [allScripts, selectedCategoryObj, searchQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCategories()
|
fetchCategories()
|
||||||
.then(setCategories)
|
.then(setCategories)
|
||||||
.catch((error) => console.error("Error fetching categories:", error));
|
.catch(error => console.error("Error fetching categories:", error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -122,11 +148,14 @@ export default function JSONGenerator() {
|
|||||||
|
|
||||||
if (updated.type === "pve") {
|
if (updated.type === "pve") {
|
||||||
scriptPath = `tools/pve/${updated.slug}.sh`;
|
scriptPath = `tools/pve/${updated.slug}.sh`;
|
||||||
} else if (updated.type === "addon") {
|
}
|
||||||
|
else if (updated.type === "addon") {
|
||||||
scriptPath = `tools/addon/${updated.slug}.sh`;
|
scriptPath = `tools/addon/${updated.slug}.sh`;
|
||||||
} else if (method.type === "alpine") {
|
}
|
||||||
|
else if (method.type === "alpine") {
|
||||||
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
scriptPath = `${updated.type}/alpine-${updated.slug}.sh`;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
scriptPath = `${updated.type}/${updated.slug}.sh`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,11 +174,13 @@ export default function JSONGenerator() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCopy = useCallback(() => {
|
const handleCopy = useCallback(() => {
|
||||||
if (!isValid) toast.warning("JSON schema is invalid. Copying anyway.");
|
if (!isValid)
|
||||||
|
toast.warning("JSON schema is invalid. Copying anyway.");
|
||||||
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
navigator.clipboard.writeText(JSON.stringify(script, null, 2));
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
setTimeout(() => setIsCopied(false), 2000);
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
if (isValid) toast.success("Copied metadata to clipboard");
|
if (isValid)
|
||||||
|
toast.success("Copied metadata to clipboard");
|
||||||
}, [script]);
|
}, [script]);
|
||||||
|
|
||||||
const importScript = (script: Script) => {
|
const importScript = (script: Script) => {
|
||||||
@@ -166,11 +197,11 @@ export default function JSONGenerator() {
|
|||||||
setIsValid(true);
|
setIsValid(true);
|
||||||
setZodErrors(null);
|
setZodErrors(null);
|
||||||
toast.success("Imported JSON successfully");
|
toast.success("Imported JSON successfully");
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
toast.error("Failed to read or parse the JSON file.");
|
toast.error("Failed to read or parse the JSON file.");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileImport = useCallback(() => {
|
const handleFileImport = useCallback(() => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement("input");
|
||||||
@@ -180,7 +211,8 @@ export default function JSONGenerator() {
|
|||||||
input.onchange = (e: Event) => {
|
input.onchange = (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
const file = target.files?.[0];
|
const file = target.files?.[0];
|
||||||
if (!file) return;
|
if (!file)
|
||||||
|
return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
@@ -189,7 +221,8 @@ export default function JSONGenerator() {
|
|||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
importScript(parsed);
|
importScript(parsed);
|
||||||
toast.success("Imported JSON successfully");
|
toast.success("Imported JSON successfully");
|
||||||
} catch (error) {
|
}
|
||||||
|
catch (error) {
|
||||||
toast.error("Failed to read the JSON file.");
|
toast.error("Failed to read the JSON file.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -243,7 +276,10 @@ export default function JSONGenerator() {
|
|||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{zodErrors.issues.map((error, index) => (
|
{zodErrors.issues.map((error, index) => (
|
||||||
<AlertDescription key={index} className="p-1 text-red-500">
|
<AlertDescription key={index} className="p-1 text-red-500">
|
||||||
{error.path.join(".")} -{error.message}
|
{error.path.join(".")}
|
||||||
|
{" "}
|
||||||
|
-
|
||||||
|
{error.message}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -270,7 +306,7 @@ export default function JSONGenerator() {
|
|||||||
onOpenChange={setIsImportDialogOpen}
|
onOpenChange={setIsImportDialogOpen}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuItem onSelect={e => e.preventDefault()}>
|
||||||
Import existing script
|
Import existing script
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -292,7 +328,7 @@ export default function JSONGenerator() {
|
|||||||
<SelectValue placeholder="Category" />
|
<SelectValue placeholder="Category" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{categories.map((category) => (
|
{categories.map(category => (
|
||||||
<SelectItem key={category.id} value={category.id.toString()}>
|
<SelectItem key={category.id} value={category.id.toString()}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -302,40 +338,44 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search for a script..."
|
placeholder="Search for a script..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
{!selectedCategory && !searchQuery ? (
|
{!selectedCategory && !searchQuery
|
||||||
<p className="text-muted-foreground text-sm text-center">
|
? (
|
||||||
Select a category or search for a script
|
<p className="text-muted-foreground text-sm text-center">
|
||||||
</p>
|
Select a category or search for a script
|
||||||
) : scripts.length === 0 ? (
|
</p>
|
||||||
<p className="text-muted-foreground text-sm text-center">
|
)
|
||||||
No scripts found
|
: scripts.length === 0
|
||||||
</p>
|
? (
|
||||||
) : (
|
<p className="text-muted-foreground text-sm text-center">
|
||||||
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
No scripts found
|
||||||
{scripts.map(script => (
|
</p>
|
||||||
<div
|
)
|
||||||
key={script.slug}
|
: (
|
||||||
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
<div className="grid grid-cols-3 auto-rows-min h-64 overflow-y-auto gap-4">
|
||||||
onClick={() => {
|
{scripts.map(script => (
|
||||||
importScript(script);
|
<div
|
||||||
setIsImportDialogOpen(false);
|
key={script.slug}
|
||||||
}}
|
className="p-2 border rounded cursor-pointer hover:bg-accent hover:text-accent-foreground"
|
||||||
>
|
onClick={() => {
|
||||||
<Image
|
importScript(script);
|
||||||
src={script.logo || `/${basePath}/logo.png`}
|
setIsImportDialogOpen(false);
|
||||||
alt={script.name}
|
}}
|
||||||
className="w-full h-12 object-contain mb-2"
|
>
|
||||||
width={16}
|
<Image
|
||||||
height={16}
|
src={script.logo || `/${basePath}/logo.png`}
|
||||||
unoptimized
|
alt={script.name}
|
||||||
/>
|
className="w-full h-12 object-contain mb-2"
|
||||||
<p className="text-sm text-center">{script.name}</p>
|
width={16}
|
||||||
</div>
|
height={16}
|
||||||
))}
|
unoptimized
|
||||||
</div>
|
/>
|
||||||
)}
|
<p className="text-sm text-center">{script.name}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -348,15 +388,19 @@ export default function JSONGenerator() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Name <span className="text-red-500">*</span>
|
Name
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input placeholder="Example" value={script.name} onChange={(e) => updateScript("name", e.target.value)} />
|
<Input placeholder="Example" value={script.name} onChange={e => updateScript("name", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Slug <span className="text-red-500">*</span>
|
Slug
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input placeholder="example" value={script.slug} onChange={(e) => updateScript("slug", e.target.value)} />
|
<Input placeholder="example" value={script.slug} onChange={e => updateScript("slug", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -366,7 +410,7 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Full logo URL"
|
placeholder="Full logo URL"
|
||||||
value={script.logo || ""}
|
value={script.logo || ""}
|
||||||
onChange={(e) => updateScript("logo", e.target.value || null)}
|
onChange={e => updateScript("logo", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -374,24 +418,28 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Path to config file"
|
placeholder="Path to config file"
|
||||||
value={script.config_path || ""}
|
value={script.config_path || ""}
|
||||||
onChange={(e) => updateScript("config_path", e.target.value || "")}
|
onChange={e => updateScript("config_path", e.target.value || "")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Description <span className="text-red-500">*</span>
|
Description
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Example"
|
placeholder="Example"
|
||||||
value={script.description}
|
value={script.description}
|
||||||
onChange={(e) => updateScript("description", e.target.value)}
|
onChange={e => updateScript("description", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Categories script={script} setScript={setScript} categories={categories} />
|
<Categories script={script} setScript={setScript} categories={categories} />
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>
|
<Label>
|
||||||
Date Created <span className="text-red-500">*</span>
|
Date Created
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild className="flex-1">
|
<PopoverTrigger asChild className="flex-1">
|
||||||
@@ -415,7 +463,7 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<Label>Type</Label>
|
<Label>Type</Label>
|
||||||
<Select value={script.type} onValueChange={(value) => updateScript("type", value)}>
|
<Select value={script.type} onValueChange={value => updateScript("type", value)}>
|
||||||
<SelectTrigger className="flex-1">
|
<SelectTrigger className="flex-1">
|
||||||
<SelectValue placeholder="Type" />
|
<SelectValue placeholder="Type" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -430,17 +478,17 @@ export default function JSONGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex gap-5">
|
<div className="w-full flex gap-5">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch checked={script.updateable} onCheckedChange={(checked) => updateScript("updateable", checked)} />
|
<Switch checked={script.updateable} onCheckedChange={checked => updateScript("updateable", checked)} />
|
||||||
<label>Updateable</label>
|
<label>Updateable</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch checked={script.privileged} onCheckedChange={(checked) => updateScript("privileged", checked)} />
|
<Switch checked={script.privileged} onCheckedChange={checked => updateScript("privileged", checked)} />
|
||||||
<label>Privileged</label>
|
<label>Privileged</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={script.disable || false}
|
checked={script.disable || false}
|
||||||
onCheckedChange={(checked) => updateScript("disable", checked)}
|
onCheckedChange={checked => updateScript("disable", checked)}
|
||||||
/>
|
/>
|
||||||
<label>Disabled</label>
|
<label>Disabled</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -448,12 +496,14 @@ export default function JSONGenerator() {
|
|||||||
{script.disable && (
|
{script.disable && (
|
||||||
<div>
|
<div>
|
||||||
<Label>
|
<Label>
|
||||||
Disable Description <span className="text-red-500">*</span>
|
Disable Description
|
||||||
|
{" "}
|
||||||
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Explain why this script is disabled..."
|
placeholder="Explain why this script is disabled..."
|
||||||
value={script.disable_description || ""}
|
value={script.disable_description || ""}
|
||||||
onChange={(e) => updateScript("disable_description", e.target.value)}
|
onChange={e => updateScript("disable_description", e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -461,18 +511,18 @@ export default function JSONGenerator() {
|
|||||||
placeholder="Interface Port"
|
placeholder="Interface Port"
|
||||||
type="number"
|
type="number"
|
||||||
value={script.interface_port || ""}
|
value={script.interface_port || ""}
|
||||||
onChange={(e) => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
onChange={e => updateScript("interface_port", e.target.value ? Number(e.target.value) : null)}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Website URL"
|
placeholder="Website URL"
|
||||||
value={script.website || ""}
|
value={script.website || ""}
|
||||||
onChange={(e) => updateScript("website", e.target.value || null)}
|
onChange={e => updateScript("website", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Documentation URL"
|
placeholder="Documentation URL"
|
||||||
value={script.documentation || ""}
|
value={script.documentation || ""}
|
||||||
onChange={(e) => updateScript("documentation", e.target.value || null)}
|
onChange={e => updateScript("documentation", e.target.value || null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<InstallMethod script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
@@ -480,22 +530,20 @@ export default function JSONGenerator() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={script.default_credentials.username || ""}
|
value={script.default_credentials.username || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
username: e.target.value || null,
|
username: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={script.default_credentials.password || ""}
|
value={script.default_credentials.password || ""}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
updateScript("default_credentials", {
|
updateScript("default_credentials", {
|
||||||
...script.default_credentials,
|
...script.default_credentials,
|
||||||
password: e.target.value || null,
|
password: e.target.value || null,
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
<Note script={script} setScript={setScript} setIsValid={setIsValid} setZodErrors={setZodErrors} />
|
||||||
</form>
|
</form>
|
||||||
@@ -504,7 +552,7 @@ export default function JSONGenerator() {
|
|||||||
<Tabs
|
<Tabs
|
||||||
defaultValue="json"
|
defaultValue="json"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onValueChange={(value) => setCurrentTab(value as "json" | "preview")}
|
onValueChange={value => setCurrentTab(value as "json" | "preview")}
|
||||||
value={currentTab}
|
value={currentTab}
|
||||||
>
|
>
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
import { ArrowRightIcon, Sparkles } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Category, Script } from "@/lib/types";
|
import type { Category, Script } from "@/lib/types";
|
||||||
@@ -21,35 +22,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/t
|
|||||||
import { DialogTitle } from "./ui/dialog";
|
import { DialogTitle } from "./ui/dialog";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export function search(scripts: Script[], query: string): Script[] {
|
|
||||||
const queryLower = query.toLowerCase().trim();
|
|
||||||
const searchWords = queryLower.split(/\s+/).filter(Boolean);
|
|
||||||
|
|
||||||
return scripts
|
|
||||||
.map(script => {
|
|
||||||
const nameLower = script.name.toLowerCase();
|
|
||||||
const descriptionLower = (script.description || "").toLowerCase();
|
|
||||||
|
|
||||||
let score = 0;
|
|
||||||
|
|
||||||
for (const word of searchWords) {
|
|
||||||
if (nameLower.includes(word)) {
|
|
||||||
score += 10;
|
|
||||||
}
|
|
||||||
if (descriptionLower.includes(word)) {
|
|
||||||
score += 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { script, score };
|
|
||||||
})
|
|
||||||
.filter(({ score }) => score > 0)
|
|
||||||
.sort((a, b) => b.score - a.score)
|
|
||||||
.slice(0, 20)
|
|
||||||
.map(({ script }) => script);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formattedBadge(type: string) {
|
export function formattedBadge(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -79,11 +51,9 @@ function getRandomScript(categories: Category[], previouslySelected: Set<string>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CommandMenu() {
|
function CommandMenu() {
|
||||||
const [query, setQuery] = React.useState("");
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [links, setLinks] = React.useState<Category[]>([]);
|
const [links, setLinks] = React.useState<Category[]>([]);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
const [results, setResults] = React.useState<Script[]>([]);
|
|
||||||
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
|
const [selectedScripts, setSelectedScripts] = React.useState<Set<string>>(new Set());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -100,27 +70,6 @@ function CommandMenu() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (query.trim() === "") {
|
|
||||||
fetchSortedCategories();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const scriptMap = new Map<string, Script>();
|
|
||||||
|
|
||||||
for (const category of links) {
|
|
||||||
for (const script of category.scripts || []) {
|
|
||||||
if (!scriptMap.has(script.slug)) {
|
|
||||||
scriptMap.set(script.slug, script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueScripts = Array.from(scriptMap.values());
|
|
||||||
const filteredResults = search(uniqueScripts, query);
|
|
||||||
setResults(filteredResults);
|
|
||||||
}
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
@@ -248,46 +197,49 @@ function CommandMenu() {
|
|||||||
|
|
||||||
<CommandDialog
|
<CommandDialog
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={setOpen}
|
||||||
setOpen(open);
|
filter={(value: string, search: string) => {
|
||||||
if (open) {
|
const searchLower = search.toLowerCase().trim();
|
||||||
setQuery("");
|
if (!searchLower)
|
||||||
setResults([]);
|
return 1;
|
||||||
}
|
const valueLower = value.toLowerCase();
|
||||||
|
const searchWords = searchLower.split(/\s+/).filter(Boolean);
|
||||||
|
// All search words must appear somewhere in the value (name + description)
|
||||||
|
const allWordsMatch = searchWords.every((word: string) => valueLower.includes(word));
|
||||||
|
return allWordsMatch ? 1 : 0;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
<DialogTitle className="sr-only">Search scripts</DialogTitle>
|
||||||
<CommandInput
|
<CommandInput placeholder="Search for a script..." />
|
||||||
placeholder="Search for a script..."
|
|
||||||
onValueChange={setQuery}
|
|
||||||
value={query}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
{isLoading ? (
|
{isLoading
|
||||||
"Searching..."
|
? (
|
||||||
) : (
|
"Searching..."
|
||||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
)
|
||||||
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
: (
|
||||||
<div className="mt-4">
|
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||||
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
<p className="text-sm text-muted-foreground">No scripts match your search.</p>
|
||||||
<Button variant="outline" size="sm" asChild>
|
<div className="mt-4">
|
||||||
<Link
|
<p className="text-xs text-muted-foreground mb-2">Want to add a new script?</p>
|
||||||
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
<Button variant="outline" size="sm" asChild>
|
||||||
target="_blank"
|
<Link
|
||||||
rel="noopener noreferrer"
|
href={`https://github.com/community-scripts/${basePath}/tree/main/docs/contribution/GUIDE.md`}
|
||||||
>
|
target="_blank"
|
||||||
Documentation <ArrowRightIcon className="ml-2 h-4 w-4" />
|
rel="noopener noreferrer"
|
||||||
</Link>
|
>
|
||||||
</Button>
|
Documentation
|
||||||
</div>
|
{" "}
|
||||||
</div>
|
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||||
)}
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
|
{Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
||||||
{results.length > 0 ? (
|
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
||||||
<CommandGroup heading="Search Results">
|
{scripts.map(script => (
|
||||||
{results.map(script => (
|
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={`script:${script.slug}`}
|
key={`script:${script.slug}`}
|
||||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
value={`${script.name} ${script.type} ${script.description || ""}`}
|
||||||
@@ -320,44 +272,7 @@ function CommandMenu() {
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
) : ( // When no search results, show all scripts grouped by category
|
))}
|
||||||
Object.entries(uniqueScriptsByCategory).map(([categoryName, scripts]) => (
|
|
||||||
<CommandGroup key={`category:${categoryName}`} heading={categoryName}>
|
|
||||||
{scripts.map(script => (
|
|
||||||
<CommandItem
|
|
||||||
key={`script:${script.slug}`}
|
|
||||||
value={`${script.name} ${script.type} ${script.description || ""}`}
|
|
||||||
onSelect={() => {
|
|
||||||
setOpen(false);
|
|
||||||
router.push(`/scripts?id=${script.slug}`);
|
|
||||||
}}
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label={`Open script ${script.name}`}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
setOpen(false);
|
|
||||||
router.push(`/scripts?id=${script.slug}`);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex gap-2" onClick={() => setOpen(false)}>
|
|
||||||
<Image
|
|
||||||
src={script.logo || `/${basePath}/logo.png`}
|
|
||||||
onError={e => ((e.currentTarget as HTMLImageElement).src = `/${basePath}/logo.png`)}
|
|
||||||
unoptimized
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
alt=""
|
|
||||||
className="h-5 w-5"
|
|
||||||
/>
|
|
||||||
<span>{script.name}</span>
|
|
||||||
<span>{formattedBadge(script.type)}</span>
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ msg_ok "Installed Tinyauth"
|
|||||||
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
||||||
|
|
||||||
cat <<EOF >/opt/tinyauth/.env
|
cat <<EOF >/opt/tinyauth/.env
|
||||||
DATABASE_PATH=/opt/tinyauth/database.db
|
TINYAUTH_DATABASE_PATH=/opt/tinyauth/database.db
|
||||||
USERS='${USER}'
|
TINYAUTH_AUTH_USERS='${USER}'
|
||||||
APP_URL=${app_url}
|
TINYAUTH_APPURL=${app_url}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
msg_info "Creating Service"
|
msg_info "Creating Service"
|
||||||
|
|||||||
62
install/tinyauth-install.sh
Normal file
62
install/tinyauth-install.sh
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2026 community-scripts ORG
|
||||||
|
# Author: MickLesk (CanbiZ)
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://github.com/steveiliop56/tinyauth
|
||||||
|
|
||||||
|
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
|
||||||
|
msg_info "Installing Dependencies"
|
||||||
|
$STD apt install -y \
|
||||||
|
openssl \
|
||||||
|
apache2-utils
|
||||||
|
msg_ok "Installed Dependencies"
|
||||||
|
|
||||||
|
fetch_and_deploy_gh_release "tinyauth" "steveiliop56/tinyauth" "singlefile" "latest" "/opt/tinyauth" "tinyauth-amd64"
|
||||||
|
|
||||||
|
msg_info "Setting up Tinyauth"
|
||||||
|
PASS=$(openssl rand -base64 8 | tr -dc 'a-zA-Z0-9' | head -c 8)
|
||||||
|
USER=$(htpasswd -Bbn "tinyauth" "${PASS}")
|
||||||
|
cat <<EOF >/opt/tinyauth/credentials.txt
|
||||||
|
Tinyauth Credentials
|
||||||
|
Username: tinyauth
|
||||||
|
Password: ${PASS}
|
||||||
|
EOF
|
||||||
|
msg_ok "Set up Tinyauth"
|
||||||
|
|
||||||
|
read -r -p "${TAB3}Enter your Tinyauth subdomain (e.g. https://tinyauth.example.com): " app_url
|
||||||
|
|
||||||
|
msg_info "Creating Service"
|
||||||
|
cat <<EOF >/opt/tinyauth/.env
|
||||||
|
TINYAUTH_DATABASE_PATH=/opt/tinyauth/database.db
|
||||||
|
TINYAUTH_AUTH_USERS='${USER}'
|
||||||
|
TINYAUTH_APPURL=${app_url}
|
||||||
|
EOF
|
||||||
|
cat <<EOF >/etc/systemd/system/tinyauth.service
|
||||||
|
[Unit]
|
||||||
|
Description=Tinyauth Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=/opt/tinyauth/.env
|
||||||
|
ExecStart=/opt/tinyauth/tinyauth
|
||||||
|
WorkingDirectory=/opt/tinyauth
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl enable -q --now tinyauth
|
||||||
|
msg_ok "Created Service"
|
||||||
|
|
||||||
|
motd_ssh
|
||||||
|
customize
|
||||||
|
cleanup_lxc
|
||||||
Reference in New Issue
Block a user