Updated admin panel, replaced with plain html and alpine js
This commit is contained in:
		@@ -47,8 +47,17 @@ func SetupRouter(params RouterParams) {
 | 
			
		||||
 | 
			
		||||
	admin := r.Group("/admin")
 | 
			
		||||
 | 
			
		||||
	// Admin page
 | 
			
		||||
	r.GET("/admin/", func(c *gin.Context) {
 | 
			
		||||
		c.File("internal/web/templates/admin/index.html")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	r.GET("/admin/oauth/gitea/callback", func(c *gin.Context) {
 | 
			
		||||
		c.File("internal/web/templates/admin/oauth-callback.html")
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	r.Use(static.Serve("/admin", static.LocalFile("internal/web/templates/admin", true)))
 | 
			
		||||
	r.Use(static.Serve("/admin/oauth/gitea/callback", static.LocalFile("internal/web/templates/admin", true)))
 | 
			
		||||
	r.Use(static.Serve("/admin/oauth/gitea", static.LocalFile("internal/web/templates/admin", true)))
 | 
			
		||||
 | 
			
		||||
	adminApi := admin.Group("/api")
 | 
			
		||||
	adminApi.POST("/login", params.AdminDashboard.Login)
 | 
			
		||||
@@ -71,4 +80,4 @@ func SetupRouter(params RouterParams) {
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										23
									
								
								internal/web/templates/admin/css/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								internal/web/templates/admin/css/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
/* Additional custom styles if needed */
 | 
			
		||||
[x-cloak] { display: none !important; }
 | 
			
		||||
 | 
			
		||||
/* Ensure body takes full height */
 | 
			
		||||
body {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Loading spinner animation */
 | 
			
		||||
.animate-spin {
 | 
			
		||||
    animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
    0% { transform: rotate(0deg); }
 | 
			
		||||
    100% { transform: rotate(360deg); }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Object fit for images */
 | 
			
		||||
.object-contain {
 | 
			
		||||
    object-fit: contain;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,25 +1,301 @@
 | 
			
		||||
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Admin Dashboard</title>
 | 
			
		||||
    <title>Admin Dashboard v2</title>
 | 
			
		||||
    <meta name="description" content="Admin Dashboard for Media Review" />
 | 
			
		||||
    <meta name="author" content="" />
 | 
			
		||||
 | 
			
		||||
    <meta property="og:title" content="Admin Dashboard" />
 | 
			
		||||
    <meta property="og:title" content="Admin Dashboard v2" />
 | 
			
		||||
    <meta property="og:description" content="Admin Dashboard for Media Review" />
 | 
			
		||||
    <meta property="og:type" content="website" />
 | 
			
		||||
 | 
			
		||||
    <meta name="twitter:card" content="summary_large_image" />
 | 
			
		||||
    <script type="module" crossorigin src="/admin/assets/index-D2PXWyfl.js"></script>
 | 
			
		||||
    <link rel="stylesheet" crossorigin href="/admin/assets/index-BvVLAYUm.css">
 | 
			
		||||
  </head>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Tailwind CSS -->
 | 
			
		||||
    <script src="https://cdn.tailwindcss.com"></script>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Alpine.js -->
 | 
			
		||||
    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
 | 
			
		||||
    <script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
    
 | 
			
		||||
    <link rel="stylesheet" href="./css/style.css">
 | 
			
		||||
    
 | 
			
		||||
    <script>
 | 
			
		||||
        // Configure tailwind
 | 
			
		||||
        tailwind.config = {
 | 
			
		||||
            darkMode: 'class',
 | 
			
		||||
            theme: {
 | 
			
		||||
                extend: {
 | 
			
		||||
                    colors: {
 | 
			
		||||
                        border: "hsl(var(--border))",
 | 
			
		||||
                        input: "hsl(var(--input))",
 | 
			
		||||
                        ring: "hsl(var(--ring))",
 | 
			
		||||
                        background: "hsl(var(--background))",
 | 
			
		||||
                        foreground: "hsl(var(--foreground))",
 | 
			
		||||
                        primary: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--primary))",
 | 
			
		||||
                            foreground: "hsl(var(--primary-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        secondary: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--secondary))",
 | 
			
		||||
                            foreground: "hsl(var(--secondary-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        destructive: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--destructive))",
 | 
			
		||||
                            foreground: "hsl(var(--destructive-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        muted: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--muted))",
 | 
			
		||||
                            foreground: "hsl(var(--muted-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        accent: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--accent))",
 | 
			
		||||
                            foreground: "hsl(var(--accent-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        popover: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--popover))",
 | 
			
		||||
                            foreground: "hsl(var(--popover-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        card: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--card))",
 | 
			
		||||
                            foreground: "hsl(var(--card-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                    },
 | 
			
		||||
                    borderRadius: {
 | 
			
		||||
                        lg: "var(--radius)",
 | 
			
		||||
                        md: "calc(var(--radius) - 2px)",
 | 
			
		||||
                        sm: "calc(var(--radius) - 4px)",
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    </script>
 | 
			
		||||
    <style type="text/tailwindcss">
 | 
			
		||||
        @layer base {
 | 
			
		||||
            :root {
 | 
			
		||||
                --background: 0 0% 100%;
 | 
			
		||||
                --foreground: 222.2 84% 4.9%;
 | 
			
		||||
                --muted: 210 40% 96.1%;
 | 
			
		||||
                --muted-foreground: 215.4 16.3% 46.9%;
 | 
			
		||||
                --popover: 0 0% 100%;
 | 
			
		||||
                --popover-foreground: 222.2 84% 4.9%;
 | 
			
		||||
                --card: 0 0% 100%;
 | 
			
		||||
                --card-foreground: 222.2 84% 4.9%;
 | 
			
		||||
                --border: 214.3 31.8% 91.4%;
 | 
			
		||||
                --input: 214.3 31.8% 91.4%;
 | 
			
		||||
                --primary: 222.2 47.4% 11.2%;
 | 
			
		||||
                --primary-foreground: 210 40% 98%;
 | 
			
		||||
                --secondary: 210 40% 96.1%;
 | 
			
		||||
                --secondary-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
                --accent: 210 40% 96.1%;
 | 
			
		||||
                --accent-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
                --destructive: 0 84.2% 60.2%;
 | 
			
		||||
                --destructive-foreground: 210 40% 98%;
 | 
			
		||||
                --ring: 215 20.2% 65.1%;
 | 
			
		||||
                --radius: 0.5rem;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            .dark {
 | 
			
		||||
                --background: 222.2 84% 4.9%;
 | 
			
		||||
                --foreground: 210 40% 98%;
 | 
			
		||||
                --muted: 217.2 32.6% 17.5%;
 | 
			
		||||
                --muted-foreground: 215 20.2% 65.1%;
 | 
			
		||||
                --popover: 222.2 84% 4.9%;
 | 
			
		||||
                --popover-foreground: 210 40% 98%;
 | 
			
		||||
                --card: 222.2 84% 4.9%;
 | 
			
		||||
                --card-foreground: 210 40% 98%;
 | 
			
		||||
                --border: 217.2 32.6% 17.5%;
 | 
			
		||||
                --input: 217.2 32.6% 17.5%;
 | 
			
		||||
                --primary: 210 40% 98%;
 | 
			
		||||
                --primary-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
                --secondary: 217.2 32.6% 17.5%;
 | 
			
		||||
                --secondary-foreground: 210 40% 98%;
 | 
			
		||||
                --accent: 217.2 32.6% 17.5%;
 | 
			
		||||
                --accent-foreground: 210 40% 98%;
 | 
			
		||||
                --destructive: 0 62.8% 30.6%;
 | 
			
		||||
                --destructive-foreground: 210 40% 98%;
 | 
			
		||||
                --ring: 212.7 26.8% 83.9%;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        @layer base {
 | 
			
		||||
            * {
 | 
			
		||||
                @apply border-border;
 | 
			
		||||
            }
 | 
			
		||||
            body {
 | 
			
		||||
                @apply bg-background text-foreground;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
 | 
			
		||||
    <div id="app" x-data="app()" x-cloak>
 | 
			
		||||
        <!-- Login View -->
 | 
			
		||||
        <template x-if="!state.token">
 | 
			
		||||
            <div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100">
 | 
			
		||||
                <div class="w-full max-w-md shadow-lg rounded-lg p-6 bg-white">
 | 
			
		||||
                    <div class="text-center pb-4">
 | 
			
		||||
                        <div class="mx-auto w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center mb-4">
 | 
			
		||||
                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6 text-slate-600">
 | 
			
		||||
                                <rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect>
 | 
			
		||||
                                <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
 | 
			
		||||
                            </svg>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <h3 class="text-2xl font-semibold text-slate-800">Admin Login</h3>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <form @submit.prevent="handleLogin" class="space-y-6">
 | 
			
		||||
                        <div class="space-y-2">
 | 
			
		||||
                            <input 
 | 
			
		||||
                                type="password" 
 | 
			
		||||
                                placeholder="Enter admin password" 
 | 
			
		||||
                                x-model="state.password"
 | 
			
		||||
                                class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent"
 | 
			
		||||
                                :disabled="state.loading"
 | 
			
		||||
                                required
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <button 
 | 
			
		||||
                            type="submit" 
 | 
			
		||||
                            class="w-full h-12 text-base bg-slate-900 text-white rounded-md hover:bg-slate-800 transition-colors disabled:opacity-50"
 | 
			
		||||
                            :disabled="state.loading"
 | 
			
		||||
                        >
 | 
			
		||||
                            <span x-show="!state.loading">Sign In</span>
 | 
			
		||||
                            <span x-show="state.loading" x-cloak>Signing in...</span>
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </form>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="relative my-6">
 | 
			
		||||
                        <div class="absolute inset-0 flex items-center">
 | 
			
		||||
                            <div class="w-full border-t border-gray-300"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="relative flex justify-center text-sm">
 | 
			
		||||
                            <span class="px-2 bg-white text-gray-500">Or continue with</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <button
 | 
			
		||||
                        @click="handleGiteaLogin"
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        class="w-full h-12 text-base border border-gray-300 rounded-md hover:bg-gray-50 transition-colors flex items-center justify-center gap-2"
 | 
			
		||||
                    >
 | 
			
		||||
                        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
 | 
			
		||||
                            <path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/>
 | 
			
		||||
                            <path d="M9 18c-4.51 2-5-2-7-2"/>
 | 
			
		||||
                        </svg>
 | 
			
		||||
                        Login with Gitea
 | 
			
		||||
                    </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <!-- Dashboard View -->
 | 
			
		||||
        <template x-if="state.token">
 | 
			
		||||
            <div class="min-h-screen">
 | 
			
		||||
                <!-- Header -->
 | 
			
		||||
                <div class="bg-white shadow-sm border-b">
 | 
			
		||||
                    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
 | 
			
		||||
                        <div class="flex justify-between items-center h-16">
 | 
			
		||||
                            <h1 class="text-xl font-semibold text-slate-800">Admin Dashboard</h1>
 | 
			
		||||
                            <button
 | 
			
		||||
                                @click="handleLogout"
 | 
			
		||||
                                class="flex items-center gap-2 border border-gray-300 rounded-md px-4 py-2 hover:bg-gray-50 transition-colors"
 | 
			
		||||
                            >
 | 
			
		||||
                                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
 | 
			
		||||
                                    <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
 | 
			
		||||
                                    <polyline points="16 17 21 12 16 7"></polyline>
 | 
			
		||||
                                    <line x1="21" x2="9" y1="12" y2="12"></line>
 | 
			
		||||
                                </svg>
 | 
			
		||||
                                Logout
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- Main Content -->
 | 
			
		||||
                <div class="flex items-center justify-center min-h-[calc(100vh-4rem)] p-8">
 | 
			
		||||
                    <div class="max-w-2xl w-full shadow-lg rounded-lg p-8 bg-white">
 | 
			
		||||
                        <template x-if="state.loading && state.mediaQueue.length === 0">
 | 
			
		||||
                            <div class="flex flex-col items-center justify-center py-16">
 | 
			
		||||
                                <div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-slate-600 mb-4"></div>
 | 
			
		||||
                                <p class="text-slate-600">Loading media...</p>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </template>
 | 
			
		||||
 | 
			
		||||
                        <template x-if="!state.loading && state.currentMedia">
 | 
			
		||||
                            <div class="space-y-6">
 | 
			
		||||
                                <!-- Image -->
 | 
			
		||||
                                <div class="flex justify-center">
 | 
			
		||||
                                    <img
 | 
			
		||||
                                        :src="state.currentMedia.preview_url"
 | 
			
		||||
                                        alt="Media to review"
 | 
			
		||||
                                        class="max-w-full max-h-96 object-contain"
 | 
			
		||||
                                        @error="handleImageError"
 | 
			
		||||
                                        x-cloak
 | 
			
		||||
                                    />
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <!-- Media Info -->
 | 
			
		||||
                                <div class="text-center text-sm text-slate-600">
 | 
			
		||||
                                    <p>Media ID: <span x-text="state.currentMedia.id"></span></p>
 | 
			
		||||
                                    <p>Post ID: <span x-text="state.currentMedia.PostID"></span></p>
 | 
			
		||||
                                    <p class="text-xs text-slate-400 mt-1" x-show="state.mediaQueue.length > 1">
 | 
			
		||||
                                        <span x-text="state.mediaQueue.length - 1"></span> more item(s) pre-loaded
 | 
			
		||||
                                    </p>
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <!-- Status Message -->
 | 
			
		||||
                                <div class="text-center" x-show="state.statusMessage" x-cloak>
 | 
			
		||||
                                    <p class="text-lg font-medium text-slate-700" x-text="state.statusMessage"></p>
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <!-- Action Buttons -->
 | 
			
		||||
                                <div class="flex gap-4 justify-center">
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        @click="handleAction('reject')"
 | 
			
		||||
                                        :disabled="state.isProcessing"
 | 
			
		||||
                                        class="flex items-center gap-2 px-8 py-3 border border-transparent rounded-md text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 transition-colors"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
 | 
			
		||||
                                            <path d="M18 6 6 18"></path>
 | 
			
		||||
                                            <path d="m6 6 12 12"></path>
 | 
			
		||||
                                        </svg>
 | 
			
		||||
                                        Reject
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <button
 | 
			
		||||
                                        @click="handleAction('approve')"
 | 
			
		||||
                                        :disabled="state.isProcessing"
 | 
			
		||||
                                        class="flex items-center gap-2 px-8 py-3 border border-transparent rounded-md text-white bg-green-600 hover:bg-green-700 disabled:opacity-50 transition-colors"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
 | 
			
		||||
                                            <polyline points="20 6 9 17 4 12"></polyline>
 | 
			
		||||
                                        </svg>
 | 
			
		||||
                                        Approve
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </template>
 | 
			
		||||
 | 
			
		||||
                        <template x-if="!state.currentMedia && !state.loading">
 | 
			
		||||
                            <div class="text-center py-16">
 | 
			
		||||
                                <p class="text-lg text-slate-600">No media available for review</p>
 | 
			
		||||
                                <button
 | 
			
		||||
                                    @click="fillMediaQueue"
 | 
			
		||||
                                    class="mt-4 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
 | 
			
		||||
                                >
 | 
			
		||||
                                    Refresh
 | 
			
		||||
                                </button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </template>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </template>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Scripts -->
 | 
			
		||||
    <script src="./js/app.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										282
									
								
								internal/web/templates/admin/js/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										282
									
								
								internal/web/templates/admin/js/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,282 @@
 | 
			
		||||
// Main application state and logic
 | 
			
		||||
function app() {
 | 
			
		||||
    return {
 | 
			
		||||
        state: {
 | 
			
		||||
            token: null,
 | 
			
		||||
            password: '',
 | 
			
		||||
            loading: false,
 | 
			
		||||
            isProcessing: false,
 | 
			
		||||
            statusMessage: '',
 | 
			
		||||
            mediaQueue: [],
 | 
			
		||||
            currentMedia: null
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        init() {
 | 
			
		||||
            // Check for existing token on component mount
 | 
			
		||||
            this.checkExistingToken();
 | 
			
		||||
            
 | 
			
		||||
            // If we have a token, start filling the media queue
 | 
			
		||||
            if (this.state.token) {
 | 
			
		||||
                // Use setTimeout to ensure Alpine is fully initialized
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    this.fillMediaQueue();
 | 
			
		||||
                }, 0);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        checkExistingToken() {
 | 
			
		||||
            const savedToken = localStorage.getItem('adminToken');
 | 
			
		||||
            if (savedToken) {
 | 
			
		||||
                try {
 | 
			
		||||
                    // Parse JWT payload to check for expiration
 | 
			
		||||
                    const payload = JSON.parse(atob(savedToken.split('.')[1]));
 | 
			
		||||
                    const currentTime = Math.floor(Date.now() / 1000);
 | 
			
		||||
                    
 | 
			
		||||
                    if (payload.exp && payload.exp > currentTime) {
 | 
			
		||||
                        this.state.token = savedToken;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Token expired, remove it
 | 
			
		||||
                        localStorage.removeItem('adminToken');
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    // Invalid token format, remove it
 | 
			
		||||
                    localStorage.removeItem('adminToken');
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        async handleLogin() {
 | 
			
		||||
            if (!this.state.password.trim()) {
 | 
			
		||||
                this.showToast("Error", "Please enter a password", "error");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.state.loading = true;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch('/admin/api/login', {
 | 
			
		||||
                    method: 'POST',
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Content-Type': 'application/json',
 | 
			
		||||
                    },
 | 
			
		||||
                    body: JSON.stringify({ Password: this.state.password })
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
 | 
			
		||||
                if (response.ok && data.token) {
 | 
			
		||||
                    this.state.token = data.token;
 | 
			
		||||
                    localStorage.setItem('adminToken', data.token);
 | 
			
		||||
                    this.showToast("Success", data.message || "Login successful");
 | 
			
		||||
                    this.state.password = '';
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.showToast("Error", data.message || "Login failed", "error");
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                this.showToast("Error", "Network error. Please try again.", "error");
 | 
			
		||||
            } finally {
 | 
			
		||||
                this.state.loading = false;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        handleGiteaLogin() {
 | 
			
		||||
            // Redirect to the Gitea OAuth endpoint
 | 
			
		||||
            window.location.href = '/admin/api/login/oauth/gitea';
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        handleLogout() {
 | 
			
		||||
            this.state.token = null;
 | 
			
		||||
            localStorage.removeItem('adminToken');
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        async fetchMedia() {
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch('/admin/api/getmedia', {
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Authorization': `Bearer ${this.state.token}`,
 | 
			
		||||
                    },
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if (response.status === 401) {
 | 
			
		||||
                    this.showToast("Session Expired", "Please login again", "error");
 | 
			
		||||
                    this.handleLogout();
 | 
			
		||||
                    // Redirect to login page after logout
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        // Trigger a page reload to update the UI based on state
 | 
			
		||||
                        location.reload();
 | 
			
		||||
                    }, 1500);
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (response.ok) {
 | 
			
		||||
                    return await response.json();
 | 
			
		||||
                } else {
 | 
			
		||||
                    const errorData = await response.json();
 | 
			
		||||
                    this.showToast("Error", errorData.message || "Failed to fetch media", "error");
 | 
			
		||||
                    return null;
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                this.showToast("Error", "Network error. Please try again.", "error");
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        async fetchNextMedia() {
 | 
			
		||||
            const media = await this.fetchMedia();
 | 
			
		||||
            if (media) {
 | 
			
		||||
                this.state.mediaQueue.push(media);
 | 
			
		||||
                
 | 
			
		||||
                // Update current media if it's the first item
 | 
			
		||||
                if (this.state.mediaQueue.length === 1) {
 | 
			
		||||
                    this.state.currentMedia = media;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Preload next items (up to 5 in queue total)
 | 
			
		||||
                if (this.state.mediaQueue.length < 5) {
 | 
			
		||||
                    this.preloadMedia();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        async fillMediaQueue() {
 | 
			
		||||
            // Don't fetch if already loading or queue is full
 | 
			
		||||
            if (this.state.loading || this.state.mediaQueue.length >= 5) return;
 | 
			
		||||
 | 
			
		||||
            // Determine how many more items we need
 | 
			
		||||
            const itemsNeeded = 5 - this.state.mediaQueue.length;
 | 
			
		||||
 | 
			
		||||
            // Fetch items one by one
 | 
			
		||||
            for (let i = 0; i < itemsNeeded; i++) {
 | 
			
		||||
                await this.fetchNextMedia();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        preloadMedia() {
 | 
			
		||||
            // Preload all items after the first one in the queue
 | 
			
		||||
            this.state.mediaQueue.slice(1, 5).forEach(media => {
 | 
			
		||||
                if (!media.preview_url) return;
 | 
			
		||||
 | 
			
		||||
                const img = new Image();
 | 
			
		||||
                img.onload = () => {
 | 
			
		||||
                    // Successful preload - nothing to do
 | 
			
		||||
                };
 | 
			
		||||
                img.onerror = () => {
 | 
			
		||||
                    // This media is broken even before we show it: remove it now
 | 
			
		||||
                    this.state.mediaQueue = this.state.mediaQueue.filter(item => item.id !== media.id);
 | 
			
		||||
                    // Update current media if needed
 | 
			
		||||
                    if (this.state.currentMedia && this.state.currentMedia.id === media.id) {
 | 
			
		||||
                        this.state.currentMedia = this.state.mediaQueue.length > 0 ? this.state.mediaQueue[0] : null;
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
                img.src = media.preview_url;
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        async handleAction(action) {
 | 
			
		||||
            if (!this.state.currentMedia) return;
 | 
			
		||||
 | 
			
		||||
            this.state.isProcessing = true;
 | 
			
		||||
            this.state.statusMessage = `${action === 'approve' ? 'Approving' : 'Rejecting'}...`;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await fetch(`/admin/api/${action}`, {
 | 
			
		||||
                    method: 'POST',
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        'Content-Type': 'application/json',
 | 
			
		||||
                        'Authorization': `Bearer ${this.state.token}`,
 | 
			
		||||
                    },
 | 
			
		||||
                    body: JSON.stringify({ mediaId: this.state.currentMedia.id }),
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                if (response.status === 401) {
 | 
			
		||||
                    this.showToast("Session Expired", "Please login again", "error");
 | 
			
		||||
                    this.handleLogout();
 | 
			
		||||
                    // Redirect to login page after logout
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        // Trigger a page reload to update the UI based on state
 | 
			
		||||
                        location.reload();
 | 
			
		||||
                    }, 1500);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const data = await response.json();
 | 
			
		||||
 | 
			
		||||
                if (response.ok) {
 | 
			
		||||
                    this.state.statusMessage = `${action === 'approve' ? 'Approved' : 'Rejected'} successfully!`;
 | 
			
		||||
 | 
			
		||||
                    // Show success message briefly, then move to the next item
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        // Remove the current item from the queue
 | 
			
		||||
                        this.state.mediaQueue.shift();
 | 
			
		||||
                        
 | 
			
		||||
                        // Update current media
 | 
			
		||||
                        if (this.state.mediaQueue.length > 0) {
 | 
			
		||||
                            this.state.currentMedia = this.state.mediaQueue[0];
 | 
			
		||||
                            // Fetch next item to keep queue full
 | 
			
		||||
                            this.fetchNextMedia();
 | 
			
		||||
                        } else {
 | 
			
		||||
                            this.state.currentMedia = null;
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        this.state.statusMessage = '';
 | 
			
		||||
                        this.state.isProcessing = false;
 | 
			
		||||
                    }, 1000);
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.showToast("Error", data.message || `Failed to ${action} media`, "error");
 | 
			
		||||
                    this.state.isProcessing = false;
 | 
			
		||||
                    this.state.statusMessage = '';
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                this.showToast("Error", "Network error. Please try again.", "error");
 | 
			
		||||
                this.state.isProcessing = false;
 | 
			
		||||
                this.state.statusMessage = '';
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        handleImageError(e) {
 | 
			
		||||
            const imgEl = e.target;
 | 
			
		||||
            if (!imgEl.dataset.fallbackTried) {
 | 
			
		||||
                imgEl.dataset.fallbackTried = 'true';
 | 
			
		||||
                imgEl.src = this.state.currentMedia.remote_url;
 | 
			
		||||
            } else {
 | 
			
		||||
                // Both preview AND remote have failed - drop it
 | 
			
		||||
                this.state.mediaQueue = this.state.mediaQueue.filter(
 | 
			
		||||
                    item => item.id !== this.state.currentMedia.id
 | 
			
		||||
                );
 | 
			
		||||
                
 | 
			
		||||
                // Update current media
 | 
			
		||||
                if (this.state.mediaQueue.length > 0) {
 | 
			
		||||
                    this.state.currentMedia = this.state.mediaQueue[0];
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.state.currentMedia = null;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        showToast(title, message, variant = 'default') {
 | 
			
		||||
            // Simple toast notification
 | 
			
		||||
            const toast = document.createElement('div');
 | 
			
		||||
            toast.className = `fixed top-4 right-4 p-4 rounded-md shadow-lg z-50 max-w-md ${
 | 
			
		||||
                variant === 'error' ? 'bg-red-100 border border-red-300 text-red-700' : 'bg-green-100 border border-green-300 text-green-700'
 | 
			
		||||
            }`;
 | 
			
		||||
            toast.innerHTML = `
 | 
			
		||||
                <div class="flex justify-between items-start">
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <div class="font-bold">${title}</div>
 | 
			
		||||
                        <div>${message}</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <button class="ml-4 text-gray-600 hover:text-gray-900" onclick="this.parentElement.parentElement.remove()">×</button>
 | 
			
		||||
                </div>
 | 
			
		||||
            `;
 | 
			
		||||
            
 | 
			
		||||
            document.body.appendChild(toast);
 | 
			
		||||
            
 | 
			
		||||
            // Auto remove after 5 seconds
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                if (toast.parentElement) {
 | 
			
		||||
                    toast.remove();
 | 
			
		||||
                }
 | 
			
		||||
            }, 5000);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										297
									
								
								internal/web/templates/admin/oauth-callback.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								internal/web/templates/admin/oauth-callback.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,297 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Completing Authentication</title>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Tailwind CSS -->
 | 
			
		||||
    <script src="https://cdn.tailwindcss.com"></script>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Alpine.js -->
 | 
			
		||||
    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    <script>
 | 
			
		||||
        // Configure tailwind
 | 
			
		||||
        tailwind.config = {
 | 
			
		||||
            darkMode: 'class',
 | 
			
		||||
            theme: {
 | 
			
		||||
                extend: {
 | 
			
		||||
                    colors: {
 | 
			
		||||
                        border: "hsl(var(--border))",
 | 
			
		||||
                        input: "hsl(var(--input))",
 | 
			
		||||
                        ring: "hsl(var(--ring))",
 | 
			
		||||
                        background: "hsl(var(--background))",
 | 
			
		||||
                        foreground: "hsl(var(--foreground))",
 | 
			
		||||
                        primary: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--primary))",
 | 
			
		||||
                            foreground: "hsl(var(--primary-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        secondary: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--secondary))",
 | 
			
		||||
                            foreground: "hsl(var(--secondary-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        destructive: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--destructive))",
 | 
			
		||||
                            foreground: "hsl(var(--destructive-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        muted: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--muted))",
 | 
			
		||||
                            foreground: "hsl(var(--muted-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        accent: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--accent))",
 | 
			
		||||
                            foreground: "hsl(var(--accent-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        popover: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--popover))",
 | 
			
		||||
                            foreground: "hsl(var(--popover-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                        card: {
 | 
			
		||||
                            DEFAULT: "hsl(var(--card))",
 | 
			
		||||
                            foreground: "hsl(var(--card-foreground))",
 | 
			
		||||
                        },
 | 
			
		||||
                    },
 | 
			
		||||
                    borderRadius: {
 | 
			
		||||
                        lg: "var(--radius)",
 | 
			
		||||
                        md: "calc(var(--radius) - 2px)",
 | 
			
		||||
                        sm: "calc(var(--radius) - 4px)",
 | 
			
		||||
                    },
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    </script>
 | 
			
		||||
    <style type="text/tailwindcss">
 | 
			
		||||
        @layer base {
 | 
			
		||||
            :root {
 | 
			
		||||
                --background: 0 0% 100%;
 | 
			
		||||
                --foreground: 222.2 84% 4.9%;
 | 
			
		||||
                --muted: 210 40% 96.1%;
 | 
			
		||||
                --muted-foreground: 215.4 16.3% 46.9%;
 | 
			
		||||
                --popover: 0 0% 100%;
 | 
			
		||||
                --popover-foreground: 222.2 84% 4.9%;
 | 
			
		||||
                --card: 0 0% 100%;
 | 
			
		||||
                --card-foreground: 222.2 84% 4.9%;
 | 
			
		||||
                --border: 214.3 31.8% 91.4%;
 | 
			
		||||
                --input: 214.3 31.8% 91.4%;
 | 
			
		||||
                --primary: 222.2 47.4% 11.2%;
 | 
			
		||||
                --primary-foreground: 210 40% 98%;
 | 
			
		||||
                --secondary: 210 40% 96.1%;
 | 
			
		||||
                --secondary-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
                --accent: 210 40% 96.1%;
 | 
			
		||||
                --accent-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
                --destructive: 0 84.2% 60.2%;
 | 
			
		||||
                --destructive-foreground: 210 40% 98%;
 | 
			
		||||
                --ring: 215 20.2% 65.1%;
 | 
			
		||||
                --radius: 0.5rem;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            .dark {
 | 
			
		||||
                --background: 222.2 84% 4.9%;
 | 
			
		||||
                --foreground: 210 40% 98%;
 | 
			
		||||
                --muted: 217.2 32.6% 17.5%;
 | 
			
		||||
                --muted-foreground: 215 20.2% 65.1%;
 | 
			
		||||
                --popover: 222.2 84% 4.9%;
 | 
			
		||||
                --popover-foreground: 210 40% 98%;
 | 
			
		||||
                --card: 222.2 84% 4.9%;
 | 
			
		||||
                --card-foreground: 210 40% 98%;
 | 
			
		||||
                --border: 217.2 32.6% 17.5%;
 | 
			
		||||
                --input: 217.2 32.6% 17.5%;
 | 
			
		||||
                --primary: 210 40% 98%;
 | 
			
		||||
                --primary-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
                --secondary: 217.2 32.6% 17.5%;
 | 
			
		||||
                --secondary-foreground: 210 40% 98%;
 | 
			
		||||
                --accent: 217.2 32.6% 17.5%;
 | 
			
		||||
                --accent-foreground: 210 40% 98%;
 | 
			
		||||
                --destructive: 0 62.8% 30.6%;
 | 
			
		||||
                --destructive-foreground: 210 40% 98%;
 | 
			
		||||
                --ring: 212.7 26.8% 83.9%;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        @layer base {
 | 
			
		||||
            * {
 | 
			
		||||
                @apply border-border;
 | 
			
		||||
            }
 | 
			
		||||
            body {
 | 
			
		||||
                @apply bg-background text-foreground;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [x-cloak] { display: none !important; }
 | 
			
		||||
        
 | 
			
		||||
        .animate-spin {
 | 
			
		||||
            animation: spin 1s linear infinite;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        @keyframes spin {
 | 
			
		||||
            0% { transform: rotate(0deg); }
 | 
			
		||||
            100% { transform: rotate(360deg); }
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
 | 
			
		||||
    <div x-data="oauthCallback()" x-init="handleOAuthCallback">
 | 
			
		||||
        <div class="min-h-screen flex items-center justify-center p-4">
 | 
			
		||||
            <div class="max-w-md w-full">
 | 
			
		||||
                <div class="bg-white rounded-2xl shadow-xl overflow-hidden">
 | 
			
		||||
                    <div class="p-8 text-center">
 | 
			
		||||
                        <div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-blue-100 mb-6">
 | 
			
		||||
                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-8 w-8 text-blue-600">
 | 
			
		||||
                                <path d="M18 6 6 18"></path>
 | 
			
		||||
                                <path d="m6 6 12 12"></path>
 | 
			
		||||
                            </svg>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                        <h2 class="text-2xl font-bold text-gray-900 mb-2">Completing Authentication</h2>
 | 
			
		||||
                        <p class="text-gray-600 mb-6">Please wait while we complete your Gitea login...</p>
 | 
			
		||||
                        
 | 
			
		||||
                        <div class="flex justify-center mb-4">
 | 
			
		||||
                            <div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500"></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        
 | 
			
		||||
                        <p class="text-sm text-gray-500">This should only take a few seconds</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <script>
 | 
			
		||||
        function oauthCallback() {
 | 
			
		||||
            return {
 | 
			
		||||
                async handleOAuthCallback() {
 | 
			
		||||
                    // Get the code from the URL parameters
 | 
			
		||||
                    const urlParams = new URLSearchParams(window.location.search);
 | 
			
		||||
                    const code = urlParams.get('code');
 | 
			
		||||
                    
 | 
			
		||||
                    if (!code) {
 | 
			
		||||
                        this.showToast("Error", "No authorization code received from Gitea", "error");
 | 
			
		||||
                        setTimeout(() => {
 | 
			
		||||
                            window.location.href = '/admin';
 | 
			
		||||
                        }, 2000);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Show processing status
 | 
			
		||||
                    document.querySelector('h2').textContent = "Verifying Credentials";
 | 
			
		||||
                    document.querySelector('p').textContent = "Checking your Gitea account information...";
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        // Send the code to our backend to exchange for a JWT token
 | 
			
		||||
                        const response = await fetch('/admin/api/login/oauth/gitea/final', {
 | 
			
		||||
                            method: 'POST',
 | 
			
		||||
                            headers: {
 | 
			
		||||
                                'Content-Type': 'application/json',
 | 
			
		||||
                            },
 | 
			
		||||
                            body: JSON.stringify({ code: code }),
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        const data = await response.json();
 | 
			
		||||
 | 
			
		||||
                        if (response.ok && data.token) {
 | 
			
		||||
                            // Store the token in localStorage
 | 
			
		||||
                            localStorage.setItem('adminToken', data.token);
 | 
			
		||||
                            
 | 
			
		||||
                            // Show success status
 | 
			
		||||
                            document.querySelector('h2').textContent = "Success!";
 | 
			
		||||
                            document.querySelector('p').textContent = "You're being redirected to the dashboard...";
 | 
			
		||||
                            
 | 
			
		||||
                            const spinner = document.querySelector('.animate-spin');
 | 
			
		||||
                            spinner.outerHTML = `
 | 
			
		||||
                                <div class="mx-auto mb-4">
 | 
			
		||||
                                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-12 w-12 text-green-500 mx-auto">
 | 
			
		||||
                                        <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
 | 
			
		||||
                                        <path d="m9 12 2 2 4-4"></path>
 | 
			
		||||
                                    </svg>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            `;
 | 
			
		||||
                            
 | 
			
		||||
                            this.showToast("Success", data.message || "Login successful");
 | 
			
		||||
                            
 | 
			
		||||
                            // Redirect to the admin dashboard after a short delay
 | 
			
		||||
                            setTimeout(() => {
 | 
			
		||||
                                window.location.href = '/admin';
 | 
			
		||||
                            }, 1500);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // Show error status
 | 
			
		||||
                            document.querySelector('h2').textContent = "Authentication Failed";
 | 
			
		||||
                            document.querySelector('p').textContent = "There was an issue with your login. Please try again.";
 | 
			
		||||
                            
 | 
			
		||||
                            const spinner = document.querySelector('.animate-spin');
 | 
			
		||||
                            spinner.outerHTML = `
 | 
			
		||||
                                <div class="mx-auto mb-4">
 | 
			
		||||
                                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-12 w-12 text-red-500 mx-auto">
 | 
			
		||||
                                        <circle cx="12" cy="12" r="10"></circle>
 | 
			
		||||
                                        <path d="M15 9l-6 6"></path>
 | 
			
		||||
                                        <path d="M9 9l6 6"></path>
 | 
			
		||||
                                    </svg>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            `;
 | 
			
		||||
                            
 | 
			
		||||
                            this.showToast("Error", data.error || "Login failed", "error");
 | 
			
		||||
                            setTimeout(() => {
 | 
			
		||||
                                window.location.href = '/admin';
 | 
			
		||||
                            }, 3000);
 | 
			
		||||
                        }
 | 
			
		||||
                    } catch (error) {
 | 
			
		||||
                        // Show error status
 | 
			
		||||
                        document.querySelector('h2').textContent = "Network Error";
 | 
			
		||||
                        document.querySelector('p').textContent = "Unable to connect to authentication server. Please try again.";
 | 
			
		||||
                        
 | 
			
		||||
                        const spinner = document.querySelector('.animate-spin');
 | 
			
		||||
                        spinner.outerHTML = `
 | 
			
		||||
                            <div class="mx-auto mb-4">
 | 
			
		||||
                                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-12 w-12 text-red-500 mx-auto">
 | 
			
		||||
                                    <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"></path>
 | 
			
		||||
                                    <path d="M9.84 9.78c-.97.42-1.81 1.2-2.34 2.23"></path>
 | 
			
		||||
                                    <path d="M14.16 9.78c.97.42 1.81 1.2 2.34 2.23"></path>
 | 
			
		||||
                                    <path d="M12 16c-.73 0-1.4-.15-2-.43"></path>
 | 
			
		||||
                                    <path d="M12 16c.73 0 1.4-.15 2-.43"></path>
 | 
			
		||||
                                </svg>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        `;
 | 
			
		||||
                        
 | 
			
		||||
                        this.showToast("Error", "Network error. Please try again.", "error");
 | 
			
		||||
                        setTimeout(() => {
 | 
			
		||||
                            window.location.href = '/admin';
 | 
			
		||||
                        }, 3000);
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                
 | 
			
		||||
                showToast(title, message, variant = 'default') {
 | 
			
		||||
                    // Remove any existing toasts
 | 
			
		||||
                    const existingToasts = document.querySelectorAll('.fixed.top-4');
 | 
			
		||||
                    existingToasts.forEach(toast => toast.remove());
 | 
			
		||||
                    
 | 
			
		||||
                    // Simple toast notification
 | 
			
		||||
                    const toast = document.createElement('div');
 | 
			
		||||
                    toast.className = `fixed top-4 right-4 p-4 rounded-md shadow-lg z-50 max-w-md ${
 | 
			
		||||
                        variant === 'error' ? 'bg-red-100 border border-red-300 text-red-700' : 'bg-green-100 border border-green-300 text-green-700'
 | 
			
		||||
                    }`;
 | 
			
		||||
                    toast.innerHTML = `
 | 
			
		||||
                        <div class="flex justify-between items-start">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <div class="font-bold">${title}</div>
 | 
			
		||||
                                <div>${message}</div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <button class="ml-4 text-gray-600 hover:text-gray-900" onclick="this.parentElement.parentElement.remove()">×</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    `;
 | 
			
		||||
                    
 | 
			
		||||
                    document.body.appendChild(toast);
 | 
			
		||||
                    
 | 
			
		||||
                    // Auto remove after 5 seconds
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        if (toast.parentElement) {
 | 
			
		||||
                            toast.remove();
 | 
			
		||||
                        }
 | 
			
		||||
                    }, 5000);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 3.2 KiB  | 
@@ -1,2 +0,0 @@
 | 
			
		||||
User-agent: *
 | 
			
		||||
Disallow: /
 | 
			
		||||
		Reference in New Issue
	
	Block a user