Compare commits

...

26 Commits

Author SHA1 Message Date
446ec4410b Fix: Add public folder to satisfy Docker COPY 2026-03-14 00:33:33 -07:00
a57099b60a Refactor: Implement trek-captcha generation 2026-03-14 00:29:47 -07:00
7a4b7464df Refactor: Remove webpack hacks for captcha 2026-03-14 00:29:38 -07:00
b2f83a2d0b Refactor: Switch to trek-captcha for compatibility 2026-03-14 00:29:31 -07:00
2a4e7bbffe Fix: Force svg-captcha external via webpack config 2026-03-14 00:21:35 -07:00
49632b9ae6 Fix: Force dynamic rendering for Captcha API 2026-03-14 00:18:47 -07:00
751bc370b8 Fix: Add svg-captcha to server external packages 2026-03-14 00:18:44 -07:00
38cb568faa Feat: Add concurrency to cancel outdated builds 2026-03-14 00:15:44 -07:00
e8fd9cbd27 Fix: Add missing autoprefixer dependency 2026-03-14 00:14:09 -07:00
3fc46631a4 Fix: Change npm ci to install and update ENV syntax 2026-03-14 00:11:35 -07:00
ab8d18bedf Feat: PostCSS configuration 2026-03-14 00:03:50 -07:00
d913e8ea01 Feat: Tailwind configuration 2026-03-14 00:03:47 -07:00
a0a639f4ae Feat: Add Tailwind directives 2026-03-14 00:03:44 -07:00
025bedb3a8 Feat: Add root layout with basic Tailwind support 2026-03-14 00:03:39 -07:00
4fd6456e28 Feat: Build frontend login page with captcha 2026-03-14 00:03:35 -07:00
cf50d0e38d Feat: Add login validation with ntfy alerts 2026-03-14 00:03:31 -07:00
8fd72e6611 Feat: Create SVG captcha generation route 2026-03-14 00:03:27 -07:00
2c76f70ac7 Config: Enable standalone output for Docker 2026-03-14 00:03:24 -07:00
80e1a56acb Feat: Setup GHCR build and push action 2026-03-14 00:03:21 -07:00
cb608f2cee Feat: Add Dockerfile for Next.js standalone build 2026-03-14 00:03:17 -07:00
eac510e6fb Feat: Initialize Next.js dependencies 2026-03-14 00:03:13 -07:00
875960a0f6 Feat: Add minimal env template for Dokploy 2026-03-14 00:03:10 -07:00
6d003b3b56 Update readme 2026-03-13 23:56:22 -07:00
177aa41990 Update readme 2026-03-13 23:34:49 -07:00
ca0d6e92c1 Add Dokploy to project technologies in readme 2026-03-13 23:31:30 -07:00
2d9972fee8 Update and rename README.md to readme 2026-03-13 23:31:12 -07:00
15 changed files with 333 additions and 1 deletions

12
.env.example Normal file
View File

@@ -0,0 +1,12 @@
# Admin Credentials
ADMIN_EMAIL=admin@kal.planetrenox.com
ADMIN_PASS=super_secure_password_meow
# Ntfy Alerts
NTFY_URL=https://ntfy.sh/my_secret_kalbot_topic
# Captcha Security (Random string)
CAPTCHA_SECRET=change_me_to_a_random_string_in_dokploy
# Application Port
PORT=3004

45
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Build and Publish Docker Image
on:
push:
branches: ['main']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

35
Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3004
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3004
CMD ["node", "server.js"]

View File

@@ -1 +0,0 @@
# KalBot

30
app/api/captcha/route.js Normal file
View File

@@ -0,0 +1,30 @@
import captcha from 'trek-captcha';
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export const dynamic = 'force-dynamic';
export async function GET() {
// Generate a 4-character alphanumeric captcha
const { token, buffer } = await captcha({ size: 4, style: -1 });
const text = token.toLowerCase();
const secret = process.env.CAPTCHA_SECRET || 'dev_secret_meow';
const hash = crypto.createHmac('sha256', secret).update(text).digest('hex');
const response = new NextResponse(buffer, {
headers: {
'Content-Type': 'image/gif',
'Cache-Control': 'no-store, max-age=0'
}
});
// Store the expected hash in an HttpOnly cookie
response.cookies.set('captcha_hash', hash, {
httpOnly: true,
path: '/',
maxAge: 300 // 5 minutes validity
});
return response;
}

38
app/api/login/route.js Normal file
View File

@@ -0,0 +1,38 @@
import { NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(req) {
try {
const body = await req.json();
const { email, password, captcha } = body;
const cookieHash = req.cookies.get('captcha_hash')?.value;
const secret = process.env.CAPTCHA_SECRET || 'dev_secret_meow';
const expectedHash = crypto.createHmac('sha256', secret).update((captcha || '').toLowerCase()).digest('hex');
if (!cookieHash || cookieHash !== expectedHash) {
return NextResponse.json({ error: 'Invalid or expired captcha' }, { status: 400 });
}
if (email === process.env.ADMIN_EMAIL && password === process.env.ADMIN_PASS) {
// Real implementation would set a JWT or session cookie here
return NextResponse.json({ success: true, message: 'Welcome back, Master!' });
} else {
// Trigger NTFY alert for failed login
if (process.env.NTFY_URL) {
await fetch(process.env.NTFY_URL, {
method: 'POST',
body: `Failed login attempt for email: ${email}`,
headers: {
'Title': 'Kalbot Login Alert',
'Priority': 'urgent',
'Tags': 'warning,skull'
}
}).catch(e => console.error("Ntfy error:", e));
}
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
} catch (err) {
return NextResponse.json({ error: 'Server error' }, { status: 500 });
}
}

3
app/globals.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

14
app/layout.js Normal file
View File

@@ -0,0 +1,14 @@
import './globals.css'
export const metadata = {
title: 'Kalbot Admin',
description: 'Kalshi bot management',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

104
app/page.js Normal file
View File

@@ -0,0 +1,104 @@
'use client';
import { useState, useRef } from 'react';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [captcha, setCaptcha] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const captchaImgRef = useRef(null);
const refreshCaptcha = () => {
if (captchaImgRef.current) {
captchaImgRef.current.src = `/api/captcha?${new Date().getTime()}`;
}
};
const handleLogin = async (e) => {
e.preventDefault();
setError('');
setSuccess('');
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, captcha })
});
const data = await res.json();
if (!res.ok) {
setError(data.error);
refreshCaptcha();
setCaptcha('');
} else {
setSuccess(data.message);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900 text-white font-sans">
<div className="bg-gray-800 p-8 rounded-lg shadow-xl w-full max-w-md">
<h1 className="text-2xl font-bold mb-6 text-center text-indigo-400">Kalbot Access</h1>
{error && <div className="bg-red-500/20 border border-red-500 text-red-300 p-3 rounded mb-4 text-sm">{error}</div>}
{success && <div className="bg-green-500/20 border border-green-500 text-green-300 p-3 rounded mb-4 text-sm">{success}</div>}
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Email</label>
<input
type="email"
required
className="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 focus:outline-none focus:border-indigo-500"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Password</label>
<input
type="password"
required
className="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 focus:outline-none focus:border-indigo-500"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Captcha Verification</label>
<div className="flex items-center space-x-3 mb-2">
<img
ref={captchaImgRef}
src="/api/captcha"
alt="captcha"
className="h-12 rounded cursor-pointer border border-gray-600"
onClick={refreshCaptcha}
title="Click to refresh"
/>
<button type="button" onClick={refreshCaptcha} className="text-sm text-indigo-400 hover:text-indigo-300">
Refresh
</button>
</div>
<input
type="text"
required
placeholder="Enter the text above"
className="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 focus:outline-none focus:border-indigo-500"
value={captcha}
onChange={(e) => setCaptcha(e.target.value)}
/>
</div>
<button
type="submit"
className="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded transition-colors mt-6"
>
Login
</button>
</form>
</div>
</div>
);
}

6
next.config.mjs Normal file
View File

@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone'
};
export default nextConfig;

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "kalbot",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev -p 3004",
"build": "next build",
"start": "next start -p $PORT",
"lint": "next lint"
},
"dependencies": {
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
"trek-captcha": "^0.4.0"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "14.2.3"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
public/.gitkeep Normal file
View File

@@ -0,0 +1 @@
I am just a placeholder so the public directory exists for Docker. Meow!

5
readme Normal file
View File

@@ -0,0 +1,5 @@
Kalshi bot @ kal.planetrenox.com
JavaScript
Next.js
Dokploy
ntfy

10
tailwind.config.js Normal file
View File

@@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
}