mirror of
https://github.com/multipleof4/KalBot.git
synced 2026-03-16 21:41: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