Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions api/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,13 @@ func main() {
auditLogger := middleware.NewAuditLogger(database, false) // Don't log request bodies by default
router.Use(auditLogger.Middleware())

// Add gzip compression (exclude WebSocket and auth endpoints)
// Add gzip compression (exclude WebSocket, auth, and metrics endpoints)
router.Use(middleware.GzipWithExclusions(
middleware.BestSpeed, // Use best speed for balance of compression vs CPU
[]string{
"/api/v1/ws/", // Exclude WebSocket paths
"/api/v1/auth/", // Exclude auth endpoints (setup, login, etc.)
"/api/v1/ws/", // Exclude WebSocket paths
"/api/v1/auth/", // Exclude auth endpoints (setup, login, etc.)
"/api/v1/metrics", // Exclude metrics (browser handles decompression inconsistently)
},
))

Expand Down
33 changes: 24 additions & 9 deletions api/internal/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1393,13 +1393,13 @@
// Repository Endpoints (Template Repository Management)
// ============================================================================

// ListRepositories returns all template repositories
// ListRepositories returns all template and plugin repositories
func (h *Handler) ListRepositories(c *gin.Context) {
// SECURITY FIX: Use request context for proper cancellation and timeout handling
ctx := c.Request.Context()

rows, err := h.db.DB().QueryContext(ctx, `
SELECT id, name, url, branch, auth_type, last_sync, template_count, status, error_message, created_at, updated_at
SELECT id, name, url, branch, COALESCE(type, 'template'), auth_type, last_sync, template_count, status, error_message, created_at, updated_at
FROM repositories
ORDER BY name ASC
`)
Expand All @@ -1412,27 +1412,42 @@
repos := []map[string]interface{}{}
for rows.Next() {
var id int
var name, url, branch, authType, status, errorMessage string
var lastSync, createdAt, updatedAt time.Time
var name, url, branch, repoType, authType, status string
var lastSync sql.NullTime

Check failure on line 1416 in api/internal/api/handlers.go

View workflow job for this annotation

GitHub Actions / Go Dependency Vulnerability Scan (api)

undefined: sql
var errorMessage sql.NullString

Check failure on line 1417 in api/internal/api/handlers.go

View workflow job for this annotation

GitHub Actions / Go Dependency Vulnerability Scan (api)

undefined: sql
var createdAt, updatedAt time.Time
var templateCount int

if err := rows.Scan(&id, &name, &url, &branch, &authType, &lastSync, &templateCount, &status, &errorMessage, &createdAt, &updatedAt); err != nil {
if err := rows.Scan(&id, &name, &url, &branch, &repoType, &authType, &lastSync, &templateCount, &status, &errorMessage, &createdAt, &updatedAt); err != nil {
continue
}

repos = append(repos, map[string]interface{}{
repo := map[string]interface{}{
"id": id,
"name": name,
"url": url,
"branch": branch,
"type": repoType,
"authType": authType,
"lastSync": lastSync,
"templateCount": templateCount,
"status": status,
"errorMessage": errorMessage,
"createdAt": createdAt,
"updatedAt": updatedAt,
})
}

if lastSync.Valid {
repo["lastSync"] = lastSync.Time
} else {
repo["lastSync"] = nil
}

if errorMessage.Valid {
repo["errorMessage"] = errorMessage.String
} else {
repo["errorMessage"] = ""
}

repos = append(repos, repo)
}

c.JSON(http.StatusOK, gin.H{
Expand Down
19 changes: 15 additions & 4 deletions api/internal/db/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,12 +363,13 @@ func (d *Database) Migrate() error {
// Create index on session_id
`CREATE INDEX IF NOT EXISTS idx_connections_session_id ON connections(session_id)`,

// Template repositories
// Template and plugin repositories
`CREATE TABLE IF NOT EXISTS repositories (
id SERIAL PRIMARY KEY,
name VARCHAR(255) UNIQUE,
url TEXT NOT NULL,
branch VARCHAR(100) DEFAULT 'main',
type VARCHAR(50) DEFAULT 'template',
auth_type VARCHAR(50) DEFAULT 'none',
auth_secret VARCHAR(255),
last_sync TIMESTAMP,
Expand All @@ -380,9 +381,9 @@ func (d *Database) Migrate() error {
)`,

// Insert default repositories (plugins and templates)
`INSERT INTO repositories (name, url, branch, auth_type, status) VALUES
('Official Plugins', 'https://github.com/JoshuaAFerguson/streamspace-plugins', 'main', 'none', 'active'),
('Official Templates', 'https://github.com/JoshuaAFerguson/streamspace-templates', 'main', 'none', 'active')
`INSERT INTO repositories (name, url, branch, type, auth_type, status) VALUES
('Official Plugins', 'https://github.com/JoshuaAFerguson/streamspace-plugins', 'main', 'plugin', 'none', 'pending'),
('Official Templates', 'https://github.com/JoshuaAFerguson/streamspace-templates', 'main', 'template', 'none', 'pending')
ON CONFLICT (name) DO NOTHING`,

// Catalog templates (cache of templates from repos)
Expand Down Expand Up @@ -523,6 +524,16 @@ func (d *Database) Migrate() error {
VALUES ('admin', 100, '64000m', '256Gi', '1Ti')
ON CONFLICT (user_id) DO NOTHING`,

// Insert default all_users group that all users belong to
`INSERT INTO groups (id, name, display_name, description, type)
VALUES ('all-users', 'all_users', 'All Users', 'Default group containing all users', 'system')
ON CONFLICT (id) DO NOTHING`,

// Add admin user to all_users group
`INSERT INTO group_memberships (id, user_id, group_id, role, created_at)
VALUES ('admin-all-users', 'admin', 'all-users', 'member', NOW())
ON CONFLICT (user_id, group_id) DO NOTHING`,

// Insert default configuration values
`INSERT INTO configuration (key, value, category, description) VALUES
('ingress.domain', 'streamspace.local', 'ingress', 'Default ingress domain'),
Expand Down
19 changes: 19 additions & 0 deletions api/internal/db/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ func (u *UserDB) CreateUser(ctx context.Context, req *models.CreateUserRequest)
return nil, fmt.Errorf("failed to create default quota: %w", err)
}

// Add user to all_users group
if err := u.addToAllUsersGroup(ctx, user.ID); err != nil {
// Log but don't fail user creation
fmt.Printf("Warning: failed to add user %s to all_users group: %v\n", user.ID, err)
}

return user, nil
}

Expand Down Expand Up @@ -576,6 +582,19 @@ func (u *UserDB) createDefaultQuota(ctx context.Context, userID string) error {
return err
}

// addToAllUsersGroup adds a user to the default all_users group
func (u *UserDB) addToAllUsersGroup(ctx context.Context, userID string) error {
query := `
INSERT INTO group_memberships (id, user_id, group_id, role, created_at)
SELECT $1, $2, id, 'member', NOW()
FROM groups WHERE name = 'all_users'
ON CONFLICT (user_id, group_id) DO NOTHING
`

_, err := u.db.ExecContext(ctx, query, uuid.New().String(), userID)
return err
}

// createQuota creates quota with custom values
func (u *UserDB) createQuota(ctx context.Context, userID string, req *models.SetQuotaRequest) error {
maxSessions := 5
Expand Down
115 changes: 75 additions & 40 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { lazy, Suspense } from 'react';
import { lazy, Suspense, useState, useMemo, createContext, useContext } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider, createTheme, CssBaseline, CircularProgress, Box } from '@mui/material';
import { useUserStore } from './store/userStore';
import ErrorBoundary from './components/ErrorBoundary';
import NotificationQueue from './components/NotificationQueue';

// Theme mode context
type ThemeMode = 'light' | 'dark';
interface ThemeContextType {
mode: ThemeMode;
toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType>({
mode: 'dark',
toggleTheme: () => {},
});

export const useThemeMode = () => useContext(ThemeContext);

// Eagerly load Login page and SetupWizard (needed immediately)
import Login from './pages/Login';
import SetupWizard from './pages/SetupWizard';
Expand All @@ -30,7 +44,6 @@ const InstalledPlugins = lazy(() => import('./pages/InstalledPlugins'));
// Admin Pages (loaded only for admin users)
const AdminDashboard = lazy(() => import('./pages/admin/Dashboard'));
const AdminNodes = lazy(() => import('./pages/admin/Nodes'));
const AdminQuotas = lazy(() => import('./pages/admin/Quotas'));
const AdminPlugins = lazy(() => import('./pages/admin/Plugins'));
const Users = lazy(() => import('./pages/admin/Users'));
const UserDetail = lazy(() => import('./pages/admin/UserDetail'));
Expand All @@ -53,34 +66,41 @@ const queryClient = new QueryClient({
},
});

// Create Material-UI theme
const theme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#3f51b5',
},
secondary: {
main: '#f50057',
// Create Material-UI theme based on mode
const createAppTheme = (mode: ThemeMode) =>
createTheme({
palette: {
mode,
primary: {
main: '#3f51b5',
},
secondary: {
main: '#f50057',
},
background:
mode === 'dark'
? {
default: '#0a1929',
paper: '#132f4c',
}
: {
default: '#f5f5f5',
paper: '#ffffff',
},
},
background: {
default: '#0a1929',
paper: '#132f4c',
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
},
components: {
MuiCard: {
styleOverrides: {
root: {
backgroundImage: 'none',
components: {
MuiCard: {
styleOverrides: {
root: {
backgroundImage: 'none',
},
},
},
},
},
});
});

// Protected Route wrapper
function ProtectedRoute({ children }: { children: React.ReactNode }) {
Expand Down Expand Up @@ -127,14 +147,36 @@ function PageLoader() {
}

function App() {
// Initialize theme from localStorage or default to dark
const [mode, setMode] = useState<ThemeMode>(() => {
const stored = localStorage.getItem('theme');
return (stored === 'light' || stored === 'dark') ? stored : 'dark';
});

const toggleTheme = () => {
setMode((prevMode) => {
const newMode = prevMode === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', newMode);
return newMode;
});
};

const theme = useMemo(() => createAppTheme(mode), [mode]);

const themeContextValue = useMemo(
() => ({ mode, toggleTheme }),
[mode]
);

return (
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<CssBaseline />
<ErrorBoundary>
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<ThemeContext.Provider value={themeContextValue}>
<ThemeProvider theme={theme}>
<CssBaseline />
<ErrorBoundary>
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
{/* Public routes */}
<Route path="/login" element={<Login />} />
<Route path="/setup" element={<SetupWizard />} />
Expand Down Expand Up @@ -212,14 +254,6 @@ function App() {
</AdminRoute>
}
/>
<Route
path="/admin/quotas"
element={
<AdminRoute>
<AdminQuotas />
</AdminRoute>
}
/>
<Route
path="/admin/plugins"
element={
Expand Down Expand Up @@ -363,7 +397,8 @@ function App() {
enableHistory={true}
maxHistorySize={50}
/>
</ThemeProvider>
</ThemeProvider>
</ThemeContext.Provider>
</QueryClientProvider>
);
}
Expand Down
3 changes: 1 addition & 2 deletions ui/src/components/AdminPortalLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ interface AdminPortalLayoutProps {
* Navigation Sections:
* - Overview: Admin dashboard
* - Content Management: Templates, Plugins, Repositories
* - User Management: Users, Groups, Quotas
* - User Management: Users, Groups
* - System: Nodes, Integrations, Scaling, Compliance
*
* @component
Expand Down Expand Up @@ -132,7 +132,6 @@ function AdminPortalLayout({ children }: AdminPortalLayoutProps) {
items: [
{ text: 'Users', icon: <PeopleIcon />, path: '/admin/users' },
{ text: 'Groups', icon: <GroupsIcon />, path: '/admin/groups' },
{ text: 'User Quotas', icon: <PeopleIcon />, path: '/admin/quotas' },
],
},
{
Expand Down
6 changes: 4 additions & 2 deletions ui/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ function Layout({ children }: LayoutProps) {
{ text: 'My Applications', icon: <AppsIcon />, path: '/' },
{ text: 'My Sessions', icon: <ComputerIcon />, path: '/sessions' },
{ text: 'Shared with Me', icon: <ShareIcon />, path: '/shared-sessions' },
{ text: 'Settings', icon: <SettingsIcon />, path: '/settings' },
];

const handleOpenAdminPortal = () => {
Expand Down Expand Up @@ -126,7 +125,10 @@ function Layout({ children }: LayoutProps) {
<Divider />
<List>
<ListItem disablePadding>
<ListItemButton>
<ListItemButton
selected={location.pathname === '/settings'}
onClick={() => navigate('/settings')}
>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
Expand Down
1 change: 1 addition & 0 deletions ui/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export interface Repository {
name: string;
url: string;
branch: string;
type: 'template' | 'plugin';
authType: string;
lastSync?: string;
templateCount: number;
Expand Down
Loading
Loading