Fix security warnings
This commit is contained in:
3
src/integrations/supabase/index.ts
Normal file
3
src/integrations/supabase/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// This file exports everything from the auto-generated client
|
||||||
|
export * from './client';
|
||||||
|
export * from './types';
|
||||||
@@ -2,18 +2,62 @@ import { useState } from "react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Server, Lock, User } from "lucide-react";
|
import { Server, Lock, Mail } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { supabase } from "@/integrations/supabase/client";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [username, setUsername] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
const [isSignup, setIsSignup] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const handleLogin = (e: React.FormEvent) => {
|
const handleAuth = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Mock login - in real app would authenticate
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isSignup) {
|
||||||
|
const { error } = await supabase.auth.signUp({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
options: {
|
||||||
|
data: {
|
||||||
|
username: email.split('@')[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Account created!",
|
||||||
|
description: "You can now log in.",
|
||||||
|
});
|
||||||
|
setIsSignup(false);
|
||||||
|
setPassword("");
|
||||||
|
} else {
|
||||||
|
const { error } = await supabase.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Authentication failed",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,24 +69,26 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-foreground mb-2">Docker WebUI</h1>
|
<h1 className="text-3xl font-bold text-foreground mb-2">Docker WebUI</h1>
|
||||||
<p className="text-muted-foreground text-center">
|
<p className="text-muted-foreground text-center">
|
||||||
Lightweight container monitoring and control
|
{isSignup ? 'Create an account to get started' : 'Lightweight container monitoring and control'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleLogin} className="space-y-4">
|
<form onSubmit={handleAuth} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="username" className="text-sm font-medium text-foreground">
|
<label htmlFor="email" className="text-sm font-medium text-foreground">
|
||||||
Username
|
Email
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
id="username"
|
id="email"
|
||||||
type="text"
|
type="email"
|
||||||
placeholder="admin"
|
placeholder="admin@example.com"
|
||||||
value={username}
|
value={email}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="pl-10 bg-secondary border-border"
|
className="pl-10 bg-secondary border-border"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,18 +106,41 @@ const Login = () => {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="pl-10 bg-secondary border-border"
|
className="pl-10 bg-secondary border-border"
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
minLength={6}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" className="w-full bg-primary hover:bg-primary/90">
|
<Button
|
||||||
Sign In
|
type="submit"
|
||||||
|
className="w-full bg-primary hover:bg-primary/90"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Processing...' : (isSignup ? 'Sign Up' : 'Sign In')}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setIsSignup(!isSignup);
|
||||||
|
setPassword("");
|
||||||
|
}}
|
||||||
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{isSignup ? 'Already have an account? Sign in' : "Don't have an account? Sign up"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isSignup && (
|
||||||
<p className="mt-6 text-xs text-center text-muted-foreground">
|
<p className="mt-6 text-xs text-center text-muted-foreground">
|
||||||
First time? You'll be prompted to change your password after login.
|
Secure authentication powered by Lovable Cloud
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1 +1,10 @@
|
|||||||
project_id = "lyqqeokmoylqtwtikwzb"
|
project_id = "lyqqeokmoylqtwtikwzb"
|
||||||
|
|
||||||
|
[functions.docker-api]
|
||||||
|
verify_jwt = true
|
||||||
|
|
||||||
|
[functions.docker-metrics]
|
||||||
|
verify_jwt = true
|
||||||
|
|
||||||
|
[functions.docker-logs]
|
||||||
|
verify_jwt = true
|
||||||
135
supabase/functions/docker-api/index.ts
Normal file
135
supabase/functions/docker-api/index.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||||
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DOCKER_SOCKET = '/var/run/docker.sock';
|
||||||
|
|
||||||
|
// Helper to make Unix socket HTTP requests to Docker API
|
||||||
|
async function dockerRequest(path: string, method = 'GET', body?: any) {
|
||||||
|
const conn = await Deno.connect({ transport: "unix", path: DOCKER_SOCKET });
|
||||||
|
|
||||||
|
const request = [
|
||||||
|
`${method} ${path} HTTP/1.1`,
|
||||||
|
'Host: localhost',
|
||||||
|
'Content-Type: application/json',
|
||||||
|
body ? `Content-Length: ${new TextEncoder().encode(JSON.stringify(body)).length}` : '',
|
||||||
|
'',
|
||||||
|
body ? JSON.stringify(body) : ''
|
||||||
|
].join('\r\n');
|
||||||
|
|
||||||
|
await conn.write(new TextEncoder().encode(request));
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const buffer = new Uint8Array(65536);
|
||||||
|
const n = await conn.read(buffer);
|
||||||
|
conn.close();
|
||||||
|
|
||||||
|
if (!n) throw new Error('No response from Docker');
|
||||||
|
|
||||||
|
const response = decoder.decode(buffer.subarray(0, n));
|
||||||
|
const [headers, ...bodyParts] = response.split('\r\n\r\n');
|
||||||
|
const responseBody = bodyParts.join('\r\n\r\n');
|
||||||
|
|
||||||
|
// Check for chunked transfer encoding
|
||||||
|
if (headers.includes('Transfer-Encoding: chunked')) {
|
||||||
|
const chunks: string[] = [];
|
||||||
|
const lines = responseBody.split('\r\n');
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const chunkSize = parseInt(lines[i], 16);
|
||||||
|
if (chunkSize === 0) break;
|
||||||
|
if (i + 1 < lines.length) {
|
||||||
|
chunks.push(lines[i + 1]);
|
||||||
|
i++; // Skip the chunk data line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JSON.parse(chunks.join(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.get('Authorization');
|
||||||
|
if (!authHeader) {
|
||||||
|
throw new Error('Missing authorization header');
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
|
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
|
||||||
|
{ global: { headers: { Authorization: authHeader } } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
||||||
|
if (userError || !user) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const path = url.pathname.replace('/docker-api', '');
|
||||||
|
|
||||||
|
// List all containers
|
||||||
|
if (path === '/containers' && req.method === 'GET') {
|
||||||
|
const containers = await dockerRequest('/containers/json?all=true');
|
||||||
|
return new Response(JSON.stringify(containers), {
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get container details
|
||||||
|
if (path.match(/^\/containers\/[^\/]+$/) && req.method === 'GET') {
|
||||||
|
const containerId = path.split('/')[2];
|
||||||
|
const details = await dockerRequest(`/containers/${containerId}/json`);
|
||||||
|
return new Response(JSON.stringify(details), {
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container actions: start, stop, restart
|
||||||
|
if (path.match(/^\/containers\/[^\/]+\/(start|stop|restart)$/) && req.method === 'POST') {
|
||||||
|
const parts = path.split('/');
|
||||||
|
const containerId = parts[2];
|
||||||
|
const action = parts[3];
|
||||||
|
|
||||||
|
const details = await dockerRequest(`/containers/${containerId}/json`);
|
||||||
|
const containerName = details.Name.replace(/^\//, '');
|
||||||
|
|
||||||
|
await dockerRequest(`/containers/${containerId}/${action}`, 'POST');
|
||||||
|
|
||||||
|
// Log to audit log
|
||||||
|
await supabase.from('audit_logs').insert({
|
||||||
|
user_id: user.id,
|
||||||
|
action: action,
|
||||||
|
container_id: containerId,
|
||||||
|
container_name: containerName,
|
||||||
|
details: { timestamp: new Date().toISOString() }
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ success: true, action, containerId }), {
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ error: 'Not found' }), {
|
||||||
|
status: 404,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Docker API error:', error);
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
return new Response(JSON.stringify({ error: message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
116
supabase/functions/docker-logs/index.ts
Normal file
116
supabase/functions/docker-logs/index.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||||
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DOCKER_SOCKET = '/var/run/docker.sock';
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return new Response(null, { headers: corsHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.get('Authorization');
|
||||||
|
if (!authHeader) {
|
||||||
|
throw new Error('Missing authorization header');
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
|
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
|
||||||
|
{ global: { headers: { Authorization: authHeader } } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
||||||
|
if (userError || !user) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const containerId = url.searchParams.get('container');
|
||||||
|
const tail = url.searchParams.get('tail') || '100';
|
||||||
|
|
||||||
|
if (!containerId) {
|
||||||
|
throw new Error('Container ID required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to Docker socket
|
||||||
|
const conn = await Deno.connect({ transport: "unix", path: DOCKER_SOCKET });
|
||||||
|
|
||||||
|
const request = `GET /containers/${containerId}/logs?stdout=true&stderr=true&tail=${tail}&follow=true HTTP/1.1\r\nHost: localhost\r\n\r\n`;
|
||||||
|
await conn.write(new TextEncoder().encode(request));
|
||||||
|
|
||||||
|
// Create a readable stream from the socket
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = new Uint8Array(8192);
|
||||||
|
let headerRead = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const n = await conn.read(buffer);
|
||||||
|
if (n === null) break;
|
||||||
|
|
||||||
|
let data = decoder.decode(buffer.subarray(0, n));
|
||||||
|
|
||||||
|
// Skip HTTP headers on first read
|
||||||
|
if (!headerRead) {
|
||||||
|
const headerEnd = data.indexOf('\r\n\r\n');
|
||||||
|
if (headerEnd !== -1) {
|
||||||
|
data = data.substring(headerEnd + 4);
|
||||||
|
headerRead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker logs use a special frame format:
|
||||||
|
// [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
|
||||||
|
// We'll parse and clean this
|
||||||
|
const lines = data.split('\n').filter(line => {
|
||||||
|
// Skip binary headers (first 8 bytes of each frame)
|
||||||
|
if (line.length > 8) {
|
||||||
|
return line.substring(8).trim().length > 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.length > 8) {
|
||||||
|
const cleaned = line.substring(8).trim();
|
||||||
|
if (cleaned) {
|
||||||
|
controller.enqueue(new TextEncoder().encode(`data: ${cleaned}\n\n`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Stream error:', error);
|
||||||
|
} finally {
|
||||||
|
conn.close();
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
...corsHeaders,
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Docker logs error:', error);
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
return new Response(JSON.stringify({ error: message }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
186
supabase/functions/docker-metrics/index.ts
Normal file
186
supabase/functions/docker-metrics/index.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
|
||||||
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
|
||||||
|
|
||||||
|
const DOCKER_SOCKET = '/var/run/docker.sock';
|
||||||
|
const HOST_PROC = '/host/proc';
|
||||||
|
|
||||||
|
// Helper to make Unix socket HTTP requests
|
||||||
|
async function dockerRequest(path: string) {
|
||||||
|
const conn = await Deno.connect({ transport: "unix", path: DOCKER_SOCKET });
|
||||||
|
|
||||||
|
const request = `GET ${path} HTTP/1.1\r\nHost: localhost\r\n\r\n`;
|
||||||
|
await conn.write(new TextEncoder().encode(request));
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const buffer = new Uint8Array(65536);
|
||||||
|
const n = await conn.read(buffer);
|
||||||
|
conn.close();
|
||||||
|
|
||||||
|
if (!n) throw new Error('No response');
|
||||||
|
|
||||||
|
const response = decoder.decode(buffer.subarray(0, n));
|
||||||
|
const [, body] = response.split('\r\n\r\n');
|
||||||
|
|
||||||
|
return JSON.parse(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read host metrics from /proc
|
||||||
|
async function getHostMetrics() {
|
||||||
|
try {
|
||||||
|
// CPU info
|
||||||
|
const cpuInfo = await Deno.readTextFile(`${HOST_PROC}/stat`);
|
||||||
|
const cpuLine = cpuInfo.split('\n')[0];
|
||||||
|
const cpuValues = cpuLine.split(/\s+/).slice(1).map(Number);
|
||||||
|
const total = cpuValues.reduce((a, b) => a + b, 0);
|
||||||
|
const idle = cpuValues[3];
|
||||||
|
const cpuUsage = ((total - idle) / total) * 100;
|
||||||
|
|
||||||
|
// Memory info
|
||||||
|
const memInfo = await Deno.readTextFile(`${HOST_PROC}/meminfo`);
|
||||||
|
const memLines = memInfo.split('\n');
|
||||||
|
const memTotal = parseInt(memLines.find(l => l.startsWith('MemTotal'))?.split(/\s+/)[1] || '0');
|
||||||
|
const memFree = parseInt(memLines.find(l => l.startsWith('MemFree'))?.split(/\s+/)[1] || '0');
|
||||||
|
const memAvailable = parseInt(memLines.find(l => l.startsWith('MemAvailable'))?.split(/\s+/)[1] || '0');
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
const uptime = await Deno.readTextFile(`${HOST_PROC}/uptime`);
|
||||||
|
const uptimeSeconds = parseFloat(uptime.split(' ')[0]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cpu: { usage: cpuUsage.toFixed(2) },
|
||||||
|
memory: {
|
||||||
|
total: memTotal,
|
||||||
|
free: memFree,
|
||||||
|
available: memAvailable,
|
||||||
|
used: memTotal - memFree,
|
||||||
|
usage: ((memTotal - memAvailable) / memTotal * 100).toFixed(2)
|
||||||
|
},
|
||||||
|
uptime: uptimeSeconds
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading host metrics:', error);
|
||||||
|
return {
|
||||||
|
cpu: { usage: 0 },
|
||||||
|
memory: { total: 0, free: 0, available: 0, used: 0, usage: 0 },
|
||||||
|
uptime: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serve(async (req) => {
|
||||||
|
const upgrade = req.headers.get("upgrade") || "";
|
||||||
|
|
||||||
|
if (upgrade.toLowerCase() !== "websocket") {
|
||||||
|
return new Response("Expected WebSocket", { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.get('Authorization');
|
||||||
|
if (!authHeader) {
|
||||||
|
return new Response("Missing authorization", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL') ?? '',
|
||||||
|
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
|
||||||
|
{ global: { headers: { Authorization: authHeader } } }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: { user }, error: userError } = await supabase.auth.getUser();
|
||||||
|
if (userError || !user) {
|
||||||
|
return new Response("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { socket, response } = Deno.upgradeWebSocket(req);
|
||||||
|
|
||||||
|
let intervalId: number;
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
console.log("WebSocket metrics connection opened");
|
||||||
|
|
||||||
|
// Send metrics every 2 seconds
|
||||||
|
intervalId = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const hostMetrics = await getHostMetrics();
|
||||||
|
const containers = await dockerRequest('/containers/json');
|
||||||
|
|
||||||
|
const containerStats = [];
|
||||||
|
for (const container of containers) {
|
||||||
|
try {
|
||||||
|
const stats = await dockerRequest(`/containers/${container.Id}/stats?stream=false`);
|
||||||
|
containerStats.push({
|
||||||
|
id: container.Id,
|
||||||
|
name: container.Names[0].replace(/^\//, ''),
|
||||||
|
cpu: calculateCPUPercent(stats),
|
||||||
|
memory: calculateMemoryUsage(stats),
|
||||||
|
network: calculateNetworkIO(stats),
|
||||||
|
blockIO: calculateBlockIO(stats)
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error getting stats for ${container.Id}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: 'metrics',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
host: hostMetrics,
|
||||||
|
containers: containerStats
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending metrics:', error);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
console.log("WebSocket connection closed");
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (e) => {
|
||||||
|
console.error("WebSocket error:", e);
|
||||||
|
if (intervalId) clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("WebSocket setup error:", error);
|
||||||
|
return new Response("Internal error", { status: 500 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions to calculate stats
|
||||||
|
function calculateCPUPercent(stats: any): string {
|
||||||
|
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
||||||
|
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
||||||
|
const cpuPercent = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
|
||||||
|
return cpuPercent.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateMemoryUsage(stats: any): { used: number; limit: number; percent: string } {
|
||||||
|
const used = stats.memory_stats.usage - (stats.memory_stats.stats?.cache || 0);
|
||||||
|
const limit = stats.memory_stats.limit;
|
||||||
|
const percent = (used / limit * 100).toFixed(2);
|
||||||
|
return { used, limit, percent };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateNetworkIO(stats: any): { rx: number; tx: number } {
|
||||||
|
const networks = stats.networks || {};
|
||||||
|
let rx = 0, tx = 0;
|
||||||
|
for (const net of Object.values(networks) as any[]) {
|
||||||
|
rx += net.rx_bytes || 0;
|
||||||
|
tx += net.tx_bytes || 0;
|
||||||
|
}
|
||||||
|
return { rx, tx };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateBlockIO(stats: any): { read: number; write: number } {
|
||||||
|
const io = stats.blkio_stats.io_service_bytes_recursive || [];
|
||||||
|
let read = 0, write = 0;
|
||||||
|
for (const entry of io) {
|
||||||
|
if (entry.op === 'Read') read += entry.value;
|
||||||
|
if (entry.op === 'Write') write += entry.value;
|
||||||
|
}
|
||||||
|
return { read, write };
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- Fix search_path for security definer functions
|
||||||
|
|
||||||
|
-- Drop and recreate update_updated_at_column with search_path
|
||||||
|
DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = now();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
|
||||||
|
|
||||||
|
-- Recreate triggers
|
||||||
|
CREATE TRIGGER update_profiles_updated_at
|
||||||
|
BEFORE UPDATE ON public.profiles
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_global_settings_updated_at
|
||||||
|
BEFORE UPDATE ON public.global_settings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
CREATE TRIGGER update_container_settings_updated_at
|
||||||
|
BEFORE UPDATE ON public.container_settings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Drop and recreate handle_new_user with proper search_path
|
||||||
|
DROP FUNCTION IF EXISTS public.handle_new_user() CASCADE;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.profiles (id, username)
|
||||||
|
VALUES (
|
||||||
|
NEW.id,
|
||||||
|
COALESCE(NEW.raw_user_meta_data->>'username', split_part(NEW.email, '@', 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO public.global_settings (user_id)
|
||||||
|
VALUES (NEW.id);
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
|
||||||
|
|
||||||
|
-- Recreate trigger
|
||||||
|
CREATE TRIGGER on_auth_user_created
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||||
Reference in New Issue
Block a user