mirror of
https://github.com/lone-cloud/prism
synced 2026-06-03 08:43:10 -07:00
code clean ups, minor improvements
This commit is contained in:
parent
2a7b1e91c6
commit
ea3345825a
20 changed files with 215 additions and 294 deletions
1
.github/workflows/release-dev.yml
vendored
1
.github/workflows/release-dev.yml
vendored
|
|
@ -30,5 +30,4 @@ jobs:
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=dev
|
VERSION=dev
|
||||||
COMMIT=${{ github.sha }}
|
|
||||||
tags: ghcr.io/lone-cloud/prism:dev
|
tags: ghcr.io/lone-cloud/prism:dev
|
||||||
|
|
|
||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -26,18 +26,17 @@ jobs:
|
||||||
- name: Build binaries
|
- name: Build binaries
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(cat VERSION)
|
VERSION=$(cat VERSION)
|
||||||
COMMIT=$(git rev-parse --short HEAD)
|
|
||||||
|
|
||||||
# Linux AMD64
|
# Linux AMD64
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||||
-trimpath \
|
-trimpath \
|
||||||
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" \
|
-ldflags="-s -w -X main.version=$VERSION" \
|
||||||
-o prism-linux-amd64 .
|
-o prism-linux-amd64 .
|
||||||
|
|
||||||
# Linux ARM64
|
# Linux ARM64
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
|
||||||
-trimpath \
|
-trimpath \
|
||||||
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" \
|
-ldflags="-s -w -X main.version=$VERSION" \
|
||||||
-o prism-linux-arm64 .
|
-o prism-linux-arm64 .
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
|
|
@ -72,7 +71,6 @@ jobs:
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ steps.version.outputs.tag }}
|
VERSION=${{ steps.version.outputs.tag }}
|
||||||
COMMIT=${{ github.sha }}
|
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/lone-cloud/prism:${{ steps.version.outputs.tag }}
|
ghcr.io/lone-cloud/prism:${{ steps.version.outputs.tag }}
|
||||||
ghcr.io/lone-cloud/prism:latest
|
ghcr.io/lone-cloud/prism:latest
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
FROM golang:1.25-alpine3.23 AS builder
|
FROM golang:1.25-alpine3.23 AS builder
|
||||||
|
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG COMMIT=unknown
|
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
|
@ -14,7 +13,7 @@ COPY . .
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||||
-trimpath \
|
-trimpath \
|
||||||
-ldflags="-w -s -X main.version=${VERSION} -X main.commit=${COMMIT}" \
|
-ldflags="-w -s -X main.version=${VERSION}" \
|
||||||
-o prism .
|
-o prism .
|
||||||
|
|
||||||
FROM debian:trixie-slim
|
FROM debian:trixie-slim
|
||||||
|
|
|
||||||
3
Makefile
3
Makefile
|
|
@ -2,14 +2,13 @@
|
||||||
|
|
||||||
BINARY_NAME=prism
|
BINARY_NAME=prism
|
||||||
VERSION?=$(shell cat VERSION 2>/dev/null || echo "dev")
|
VERSION?=$(shell cat VERSION 2>/dev/null || echo "dev")
|
||||||
COMMIT?=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
||||||
GOBIN?=$(shell command -v go >/dev/null 2>&1 && go env GOPATH || echo "${HOME}/go")/bin
|
GOBIN?=$(shell command -v go >/dev/null 2>&1 && go env GOPATH || echo "${HOME}/go")/bin
|
||||||
export PATH := $(GOBIN):$(PATH)
|
export PATH := $(GOBIN):$(PATH)
|
||||||
|
|
||||||
all: fix build
|
all: fix build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -ldflags="-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT)" -o $(BINARY_NAME) .
|
go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY_NAME) .
|
||||||
|
|
||||||
start: build
|
start: build
|
||||||
./$(BINARY_NAME)
|
./$(BINARY_NAME)
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.2.1
|
0.2.2
|
||||||
3
main.go
3
main.go
|
|
@ -21,7 +21,6 @@ var publicAssets embed.FS
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "dev"
|
version = "dev"
|
||||||
commit = "unknown"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -30,7 +29,7 @@ func init() {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) > 1 && os.Args[1] == "version" {
|
if len(os.Args) > 1 && os.Args[1] == "version" {
|
||||||
fmt.Printf("Prism %s (%s)\n", version, commit)
|
fmt.Printf("Prism %s\n", version)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* Variables */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #f5f5f5;
|
--bg-primary: #f5f5f5;
|
||||||
--bg-secondary: #fdfdfd;
|
--bg-secondary: #fdfdfd;
|
||||||
|
|
@ -35,7 +36,7 @@
|
||||||
--spinner-track: #4a4a4a;
|
--spinner-track: #4a4a4a;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CSS Reset */
|
/* Reset */
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
|
|
@ -78,9 +79,11 @@ h6 {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles */
|
/* Base */
|
||||||
body {
|
body {
|
||||||
font-family: system-ui;
|
font-family: system-ui;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
max-width: 50rem;
|
max-width: 50rem;
|
||||||
margin: 1rem auto;
|
margin: 1rem auto;
|
||||||
padding: 0 1.25rem 1.25rem;
|
padding: 0 1.25rem 1.25rem;
|
||||||
|
|
@ -88,6 +91,22 @@ body {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Components */
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
.card {
|
.card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
@ -128,19 +147,7 @@ details[open] > .card-header::before {
|
||||||
padding: 0 1.25rem 1.25rem 1.25rem;
|
padding: 0 1.25rem 1.25rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
/* Tooltips */
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--accent);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.channel-badge:has(.tooltip),
|
.channel-badge:has(.tooltip),
|
||||||
.integration-status:has(.tooltip) {
|
.integration-status:has(.tooltip) {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
|
|
@ -183,6 +190,7 @@ a:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Theme Toggle */
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1.5rem;
|
bottom: 1.5rem;
|
||||||
|
|
@ -201,6 +209,7 @@ a:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* App List */
|
||||||
.app-list {
|
.app-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -300,6 +309,7 @@ a:hover {
|
||||||
filter: brightness(0.85);
|
filter: brightness(0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
.loading {
|
.loading {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -322,6 +332,7 @@ a:hover {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Integrations */
|
||||||
.integration-card {
|
.integration-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
@ -405,6 +416,7 @@ details[open] > .integration-header::before {
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
.auth-form {
|
.auth-form {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
|
@ -433,6 +445,7 @@ details[open] > .integration-header::before {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
.btn-primary,
|
.btn-primary,
|
||||||
.btn-secondary,
|
.btn-secondary,
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
|
|
@ -500,6 +513,7 @@ details[open] > .integration-header::before {
|
||||||
color: var(--text-on-color);
|
color: var(--text-on-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* QR Code */
|
||||||
.qr-container {
|
.qr-container {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Prism Admin</title>
|
<title>Prism Admin</title>
|
||||||
<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="#8159b8">
|
<meta name="theme-color" content="#60c5ff">
|
||||||
<link rel="icon" type="image/webp" href="/favicon.webp">
|
<link rel="icon" type="image/webp" href="/favicon.webp">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<link rel="stylesheet" href="/index.css?v={{.Version}}">
|
<link rel="stylesheet" href="/index.css?v={{.Version}}">
|
||||||
|
|
|
||||||
|
|
@ -1,220 +1,169 @@
|
||||||
document.addEventListener('submit', async (e) => {
|
async function handleAuthForm(form, endpoint, statusId, getPayload) {
|
||||||
if (e.target.id === 'telegram-auth-form') {
|
const status = document.getElementById(statusId);
|
||||||
e.preventDefault();
|
const btn = form.querySelector('button[type="submit"]');
|
||||||
const form = e.target;
|
const showError = (msg) => {
|
||||||
const status = document.getElementById('telegram-auth-status');
|
status.textContent = `Error: ${msg}`;
|
||||||
const btn = form.querySelector('button[type="submit"]');
|
status.className = 'auth-status error';
|
||||||
|
btn.disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const response = await fetch('/api/telegram/auth', {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify(getPayload(formData, form)),
|
||||||
},
|
});
|
||||||
body: JSON.stringify({
|
|
||||||
bot_token: formData.get('bot_token'),
|
|
||||||
chat_id: formData.get('chat_id'),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
status.textContent = `Error: ${error.error || 'Failed to save'}`;
|
showError(error.error || 'Failed to save');
|
||||||
status.className = 'auth-status error';
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
status.textContent = `Error: ${err.message}`;
|
|
||||||
status.className = 'auth-status error';
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showError(err.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (e.target.id === 'telegram-chatid-form') {
|
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
|
||||||
e.preventDefault();
|
async function submitTelegramAuth(e) {
|
||||||
const form = e.target;
|
e.preventDefault();
|
||||||
const status = document.getElementById('telegram-chatid-status');
|
await handleAuthForm(
|
||||||
const btn = form.querySelector('button[type="submit"]');
|
e.target,
|
||||||
|
'/api/telegram/auth',
|
||||||
|
'telegram-auth-status',
|
||||||
|
(fd) => ({
|
||||||
|
bot_token: fd.get('bot_token'),
|
||||||
|
chat_id: fd.get('chat_id'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
btn.disabled = true;
|
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
|
||||||
|
async function submitTelegramChatId(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
await handleAuthForm(
|
||||||
|
e.target,
|
||||||
|
'/api/telegram/auth',
|
||||||
|
'telegram-chatid-status',
|
||||||
|
(fd, form) => ({
|
||||||
|
bot_token: form.dataset.botToken,
|
||||||
|
chat_id: fd.get('chat_id'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
|
||||||
const formData = new FormData(form);
|
async function submitProtonAuth(e) {
|
||||||
const botToken = form.dataset.botToken;
|
e.preventDefault();
|
||||||
const response = await fetch('/api/telegram/auth', {
|
await handleAuthForm(
|
||||||
method: 'POST',
|
e.target,
|
||||||
headers: {
|
'/api/proton/auth',
|
||||||
'Content-Type': 'application/json',
|
'proton-auth-status',
|
||||||
},
|
(fd) => ({
|
||||||
body: JSON.stringify({
|
email: fd.get('email'),
|
||||||
bot_token: botToken,
|
password: fd.get('password'),
|
||||||
chat_id: formData.get('chat_id'),
|
totp: fd.get('totp'),
|
||||||
}),
|
}),
|
||||||
});
|
);
|
||||||
|
}
|
||||||
if (response.ok) {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
status.textContent = `Error: ${error.error || 'Failed to save'}`;
|
|
||||||
status.className = 'auth-status error';
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
status.textContent = `Error: ${err.message}`;
|
|
||||||
status.className = 'auth-status error';
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.id === 'proton-auth-form') {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const status = document.getElementById('proton-auth-status');
|
|
||||||
const btn = form.querySelector('button[type="submit"]');
|
|
||||||
|
|
||||||
btn.disabled = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const response = await fetch('/api/proton/auth', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: formData.get('email'),
|
|
||||||
password: formData.get('password'),
|
|
||||||
totp: formData.get('totp'),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
status.textContent = `Error: ${error.error || 'Authentication failed'}`;
|
|
||||||
status.className = 'auth-status error';
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
status.textContent = `Error: ${err.message}`;
|
|
||||||
status.className = 'auth-status error';
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let signalLinkingPoll = null;
|
let signalLinkingPoll = null;
|
||||||
|
|
||||||
document.addEventListener('click', async (e) => {
|
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
|
||||||
if (e.target.id === 'signal-link-btn') {
|
async function linkSignal(btn) {
|
||||||
const btn = e.target;
|
const qrContainer = document.getElementById('signal-qr-container');
|
||||||
const qrContainer = document.getElementById('signal-qr-container');
|
const qrCode = document.getElementById('signal-qr-code');
|
||||||
const qrCode = document.getElementById('signal-qr-code');
|
const showQrError = (msg) => {
|
||||||
|
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${msg}</p>`;
|
||||||
|
qrContainer.style.display = 'block';
|
||||||
|
btn.style.display = 'inline-block';
|
||||||
|
};
|
||||||
|
|
||||||
btn.style.display = 'none';
|
btn.style.display = 'none';
|
||||||
qrContainer.style.display = 'none';
|
qrContainer.style.display = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/signal/link', {
|
const response = await fetch('/api/signal/link', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
body: JSON.stringify({ device_name: 'Prism' }),
|
||||||
},
|
});
|
||||||
body: JSON.stringify({ device_name: 'Prism' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${error.error || 'Failed to generate link'}</p>`;
|
showQrError(error.error || 'Failed to generate link');
|
||||||
qrContainer.style.display = 'block';
|
|
||||||
btn.style.display = 'inline-block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(data.qr_code)}`;
|
|
||||||
qrCode.src = qrUrl;
|
|
||||||
qrContainer.style.display = 'block';
|
|
||||||
|
|
||||||
signalLinkingPoll = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const statusResp = await fetch('/api/signal/status');
|
|
||||||
const statusData = await statusResp.json();
|
|
||||||
|
|
||||||
if (statusData.linked) {
|
|
||||||
clearInterval(signalLinkingPoll);
|
|
||||||
qrContainer.innerHTML =
|
|
||||||
'<p class="auth-status success">Linked! Refreshing...</p>';
|
|
||||||
setTimeout(() => location.reload(), 1000);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Status check failed:', err);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
} catch (err) {
|
|
||||||
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${err.message}</p>`;
|
|
||||||
qrContainer.style.display = 'block';
|
|
||||||
btn.style.display = 'inline-block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.classList.contains('reload-btn')) {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.target.classList.contains('delete-telegram-btn')) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!confirm('Unlink Telegram integration?')) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const btn = e.target;
|
const data = await response.json();
|
||||||
btn.disabled = true;
|
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(data.qr_code)}`;
|
||||||
|
qrCode.src = qrUrl;
|
||||||
|
qrContainer.style.display = 'block';
|
||||||
|
|
||||||
try {
|
signalLinkingPoll = setInterval(async () => {
|
||||||
const response = await fetch('/api/telegram/auth', {
|
try {
|
||||||
method: 'DELETE',
|
const statusResp = await fetch('/api/signal/status');
|
||||||
});
|
const statusData = await statusResp.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (statusData.linked) {
|
||||||
location.reload();
|
clearInterval(signalLinkingPoll);
|
||||||
} else {
|
qrContainer.innerHTML =
|
||||||
const error = await response.json();
|
'<p class="auth-status success">Linked! Refreshing...</p>';
|
||||||
alert(`Error: ${error.error || 'Failed to unlink integration'}`);
|
setTimeout(() => location.reload(), 1000);
|
||||||
btn.disabled = false;
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Status check failed:', err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}, 2000);
|
||||||
alert(`Error: ${err.message}`);
|
} catch (err) {
|
||||||
|
showQrError(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
|
||||||
|
async function deleteTelegram(btn) {
|
||||||
|
if (!confirm('Unlink Telegram integration?')) return;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/telegram/auth', { method: 'DELETE' });
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert(`Error: ${error.error || 'Failed to unlink integration'}`);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Error: ${err.message}`);
|
||||||
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (e.target.classList.contains('delete-proton-btn')) {
|
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
|
||||||
if (!confirm('Unlink Proton Mail integration?')) {
|
async function deleteProton(btn) {
|
||||||
return;
|
if (!confirm('Unlink Proton Mail integration?')) return;
|
||||||
}
|
|
||||||
|
btn.disabled = true;
|
||||||
try {
|
|
||||||
const response = await fetch('/api/proton/auth', {
|
try {
|
||||||
method: 'DELETE',
|
const response = await fetch('/api/proton/auth', { method: 'DELETE' });
|
||||||
});
|
|
||||||
|
if (response.ok) {
|
||||||
if (response.ok) {
|
location.reload();
|
||||||
location.reload();
|
} else {
|
||||||
} else {
|
const error = await response.json();
|
||||||
const error = await response.json();
|
alert(`Error: ${error.error || 'Failed to unlink integration'}`);
|
||||||
alert(`Error: ${error.error || 'Failed to unlink integration'}`);
|
btn.disabled = false;
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Error: ${err.message}`);
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Error: ${err.message}`);
|
||||||
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#1a1a1a",
|
"background_color": "#1a1a1a",
|
||||||
"theme_color": "#8159b8",
|
"theme_color": "#60c5ff",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/favicon.webp",
|
"src": "/favicon.webp",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
const themes = ['system', 'light', 'dark'];
|
|
||||||
let currentIndex = 0;
|
|
||||||
|
|
||||||
const savedTheme = localStorage.getItem('theme') || 'system';
|
const savedTheme = localStorage.getItem('theme') || 'system';
|
||||||
currentIndex = themes.indexOf(savedTheme);
|
const themes = ['system', 'light', 'dark'];
|
||||||
|
let currentIndex = themes.indexOf(savedTheme);
|
||||||
|
|
||||||
if (currentIndex === -1) currentIndex = 0;
|
if (currentIndex === -1) currentIndex = 0;
|
||||||
|
|
||||||
function applyTheme(theme) {
|
function applyTheme(theme) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{{if .Connected}}
|
{{if .Connected}}
|
||||||
<button class="btn-danger delete-proton-btn">Unlink</button>
|
<button class="btn-danger" onclick="deleteProton(this)">Unlink</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p><strong>Link Proton Account:</strong></p>
|
<p><strong>Link Proton Account:</strong></p>
|
||||||
<form id="proton-auth-form" class="auth-form">
|
<form class="auth-form" onsubmit="submitProtonAuth(event)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="proton-email">Email:</label>
|
<label for="proton-email">Email:</label>
|
||||||
<input type="email" id="proton-email" name="email" placeholder="your-email@proton.me" required>
|
<input type="email" id="proton-email" name="email" placeholder="your-email@proton.me" required>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
{{if .Error}}
|
{{if .Error}}
|
||||||
<p class="channel-not-configured">{{.Error}}</p>
|
<p class="channel-not-configured">{{.Error}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button id="signal-link-btn" class="btn-primary">Link</button>
|
<button class="btn-primary" onclick="linkSignal(this)">Link</button>
|
||||||
<div id="signal-qr-container" class="qr-container" style="display:none;">
|
<div id="signal-qr-container" class="qr-container" style="display:none;">
|
||||||
<p><strong>Scan this QR code with Signal:</strong></p>
|
<p><strong>Scan this QR code with Signal:</strong></p>
|
||||||
<ol class="link-instructions">
|
<ol class="link-instructions">
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<li>Message <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a> to get your Chat ID</li>
|
<li>Message <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a> to get your Chat ID</li>
|
||||||
<li>Enter both below:</li>
|
<li>Enter both below:</li>
|
||||||
</ol>
|
</ol>
|
||||||
<form id="telegram-auth-form" class="auth-form">
|
<form class="auth-form" onsubmit="submitTelegramAuth(event)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="telegram-bot-token">Bot Token:</label>
|
<label for="telegram-bot-token">Bot Token:</label>
|
||||||
<input type="text" id="telegram-bot-token" name="bot_token" placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" required>
|
<input type="text" id="telegram-bot-token" name="bot_token" placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" required>
|
||||||
|
|
@ -21,11 +21,11 @@
|
||||||
</form>
|
</form>
|
||||||
{{else if .Error}}
|
{{else if .Error}}
|
||||||
<p>Error: {{.Error}}</p>
|
<p>Error: {{.Error}}</p>
|
||||||
<button class="btn-secondary reload-btn">Retry</button>
|
<button class="btn-secondary" onclick="location.reload()">Retry</button>
|
||||||
{{else if .NeedsChatID}}
|
{{else if .NeedsChatID}}
|
||||||
<p><strong>Complete Setup:</strong></p>
|
<p><strong>Complete Setup:</strong></p>
|
||||||
<p>Bot is configured, but Chat ID is missing.</p>
|
<p>Bot is configured, but Chat ID is missing.</p>
|
||||||
<form id="telegram-chatid-form" class="auth-form" data-bot-token="{{.BotToken}}">
|
<form class="auth-form" data-bot-token="{{.BotToken}}" onsubmit="submitTelegramChatId(event)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="telegram-chat-id-only">Chat ID:</label>
|
<label for="telegram-chat-id-only">Chat ID:</label>
|
||||||
<input type="text" id="telegram-chat-id-only" name="chat_id" placeholder="Get from @userinfobot" required>
|
<input type="text" id="telegram-chat-id-only" name="chat_id" placeholder="Get from @userinfobot" required>
|
||||||
|
|
@ -34,6 +34,6 @@
|
||||||
<div id="telegram-chatid-status" class="auth-status"></div>
|
<div id="telegram-chatid-status" class="auth-status"></div>
|
||||||
</form>
|
</form>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button class="btn-danger delete-telegram-btn">Unlink</button>
|
<button class="btn-danger" onclick="deleteTelegram(this)">Unlink</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"prism/service/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
if err := s.indexTmpl.Execute(w, map[string]string{"Version": s.version}); err != nil {
|
|
||||||
util.LogAndError(w, s.logger, "Internal Server Error", http.StatusInternalServerError, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,6 +10,22 @@ import (
|
||||||
"prism/service/util"
|
"prism/service/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AppListItem struct {
|
||||||
|
AppName string
|
||||||
|
Channel string
|
||||||
|
ChannelBadge string
|
||||||
|
ChannelConfigured bool
|
||||||
|
Tooltip string
|
||||||
|
Hostname string
|
||||||
|
ChannelOptions []SelectOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectOption struct {
|
||||||
|
Value string
|
||||||
|
Label string
|
||||||
|
Selected bool
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleFragmentApps(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleFragmentApps(w http.ResponseWriter, r *http.Request) {
|
||||||
mappings, err := s.store.GetAllMappings()
|
mappings, err := s.store.GetAllMappings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,11 @@ func (s *Server) setupRoutes() {
|
||||||
|
|
||||||
r.Get("/health", s.handleHealthCheck)
|
r.Get("/health", s.handleHealthCheck)
|
||||||
r.Head("/health", s.handleHealthCheck)
|
r.Head("/health", s.handleHealthCheck)
|
||||||
r.Get("/", s.handleIndex)
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := s.indexTmpl.Execute(w, map[string]string{"Version": s.version}); err != nil {
|
||||||
|
util.LogAndError(w, s.logger, "Internal Server Error", http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/favicon.webp", http.StatusMovedPermanently)
|
http.Redirect(w, r, "/favicon.webp", http.StatusMovedPermanently)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import "html/template"
|
|
||||||
|
|
||||||
type AppListItem struct {
|
|
||||||
AppName string
|
|
||||||
Channel string
|
|
||||||
ChannelBadge string
|
|
||||||
ChannelConfigured bool
|
|
||||||
Tooltip string
|
|
||||||
Hostname string
|
|
||||||
ChannelOptions []SelectOption
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectOption struct {
|
|
||||||
Value string
|
|
||||||
Label string
|
|
||||||
Selected bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type IntegrationData struct {
|
|
||||||
Name string
|
|
||||||
StatusClass string
|
|
||||||
StatusText string
|
|
||||||
StatusTooltip string
|
|
||||||
Content template.HTML
|
|
||||||
Open bool
|
|
||||||
PollAttrs string
|
|
||||||
}
|
|
||||||
|
|
@ -77,17 +77,8 @@ func isLocalIP(addr string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetClientIP(r *http.Request) string {
|
func GetClientIP(r *http.Request) string {
|
||||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
host, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||||
parts := strings.Split(xff, ",")
|
if host == "" {
|
||||||
return strings.TrimSpace(parts[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
|
||||||
return xri
|
|
||||||
}
|
|
||||||
|
|
||||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
return r.RemoteAddr
|
return r.RemoteAddr
|
||||||
}
|
}
|
||||||
return host
|
return host
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func LogAndError(w http.ResponseWriter, logger *slog.Logger, message string, code int, err error, attrs ...any) {
|
func LogAndError(w http.ResponseWriter, logger *slog.Logger, message string, code int, err error, attrs ...any) {
|
||||||
if err != nil {
|
logAttrs := append([]any{"error", err}, attrs...)
|
||||||
logAttrs := append([]any{"error", err}, attrs...)
|
logger.Error(message, logAttrs...)
|
||||||
logger.Error(message, logAttrs...)
|
|
||||||
} else {
|
|
||||||
logger.Error(message, attrs...)
|
|
||||||
}
|
|
||||||
http.Error(w, message, code)
|
http.Error(w, message, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue