mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-17 05:51:02 +00:00
Compare commits
26 Commits
9c3e546045
...
446ec4410b
| Author | SHA1 | Date | |
|---|---|---|---|
| 446ec4410b | |||
| a57099b60a | |||
| 7a4b7464df | |||
| b2f83a2d0b | |||
| 2a4e7bbffe | |||
| 49632b9ae6 | |||
| 751bc370b8 | |||
| 38cb568faa | |||
| e8fd9cbd27 | |||
| 3fc46631a4 | |||
| ab8d18bedf | |||
| d913e8ea01 | |||
| a0a639f4ae | |||
| 025bedb3a8 | |||
| 4fd6456e28 | |||
| cf50d0e38d | |||
| 8fd72e6611 | |||
| 2c76f70ac7 | |||
| 80e1a56acb | |||
| cb608f2cee | |||
| eac510e6fb | |||
| 875960a0f6 | |||
| 6d003b3b56 | |||
| 177aa41990 | |||
| ca0d6e92c1 | |||
| 2d9972fee8 |
12
.env.example
Normal file
12
.env.example
Normal 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
45
.github/workflows/docker.yml
vendored
Normal 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
35
Dockerfile
Normal 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"]
|
||||||
30
app/api/captcha/route.js
Normal file
30
app/api/captcha/route.js
Normal 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
38
app/api/login/route.js
Normal 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
3
app/globals.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
14
app/layout.js
Normal file
14
app/layout.js
Normal 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
104
app/page.js
Normal 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
6
next.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
output: 'standalone'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
24
package.json
Normal file
24
package.json
Normal 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
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
public/.gitkeep
Normal file
1
public/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
I am just a placeholder so the public directory exists for Docker. Meow!
|
||||||
5
readme
Normal file
5
readme
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Kalshi bot @ kal.planetrenox.com
|
||||||
|
JavaScript
|
||||||
|
Next.js
|
||||||
|
Dokploy
|
||||||
|
ntfy
|
||||||
10
tailwind.config.js
Normal file
10
tailwind.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user