24 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
0c0c9c7f23 Fix first user not getting ADMIN role when signing up via OAuth
The createUser event in auth.ts now promotes the first user to ADMIN
if no admin exists yet. The JWT callback also fetches the role from the
database on sign-in to pick up the freshly assigned ADMIN role.

Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 19:21:25 +00:00
copilot-swe-agent[bot]
82d5fc1812 Initial plan 2026-03-04 19:15:27 +00:00
xCyanGrizzly
9120f0fb5d Merge pull request #7 from xCyanGrizzly/copilot/fix-telegram-page-redirect
Fix telegram page redirect: auto-admin first user, hide admin-only nav
2026-03-04 20:06:12 +01:00
copilot-swe-agent[bot]
5d88f9beb3 Wrap first-user admin check in transaction to prevent race condition
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 18:55:41 +00:00
copilot-swe-agent[bot]
3704708970 Fix telegram page redirect: make first user admin and hide admin-only nav items from non-admins
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 18:55:02 +00:00
copilot-swe-agent[bot]
0c789eabd6 Initial plan 2026-03-04 18:24:56 +00:00
xCyanGrizzly
9a88914f11 Merge pull request #6 from xCyanGrizzly/copilot/fix-module-not-found-error
Fix: replace selective node_modules allowlist with full copy to prevent missing Prisma CLI deps
2026-03-04 17:51:05 +01:00
copilot-swe-agent[bot]
6cc8e1185a Fix: Copy full node_modules to production image to prevent missing module errors
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 15:31:01 +00:00
copilot-swe-agent[bot]
066fb5a046 Fix: Copy valibot to production Docker image for Prisma CLI
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 15:22:59 +00:00
copilot-swe-agent[bot]
bed99f8167 Initial plan 2026-03-04 15:20:17 +00:00
xCyanGrizzly
80a8833f2c Merge pull request #5 from xCyanGrizzly/copilot/fix-prisma-schema-error
Fix ENOENT for prisma_schema_build_bg.wasm in production Docker image
2026-03-04 16:17:55 +01:00
copilot-swe-agent[bot]
7303d5c6d3 Fix missing prisma_schema_build_bg.wasm by using symlink for .bin/prisma in Dockerfile
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 14:37:15 +00:00
copilot-swe-agent[bot]
c5ca9a7460 Initial plan 2026-03-04 14:31:43 +00:00
xCyanGrizzly
186aae38b5 Merge pull request #4 from xCyanGrizzly/copilot/fix-portainer-deployment-issues
fix: replace pull_policy: build with pull_policy: never to unbreak Portainer redeploy
2026-03-04 15:25:30 +01:00
copilot-swe-agent[bot]
df006636de fix: change pull_policy from build to never; remove dead fonts.ts
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 14:21:46 +00:00
copilot-swe-agent[bot]
373f1f2f08 Initial plan 2026-03-04 14:01:13 +00:00
xCyanGrizzly
81d322a91c Merge pull request #3 from xCyanGrizzly/copilot/fix-redeploy-error
fix: eliminate Google Fonts CDN dependency from Docker builds
2026-03-04 14:55:27 +01:00
copilot-swe-agent[bot]
48829e2efc fix: switch to local geist fonts and use direct prisma binary in entrypoint
- Replace next/font/google Geist imports with geist npm package to eliminate
  Google Fonts CDN network dependency during Docker image builds (was causing
  intermittent build failures → redeployment errors)
- Use ./node_modules/.bin/prisma directly in docker-entrypoint.sh instead of
  npx for both migrate deploy and db seed commands

Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 13:51:28 +00:00
copilot-swe-agent[bot]
b11948d3b3 Initial plan
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 13:48:30 +00:00
copilot-swe-agent[bot]
ba6bad58fa Initial plan 2026-03-04 13:34:56 +00:00
xCyanGrizzly
30fe7d6855 Merge pull request #2 from xCyanGrizzly/copilot/fix-redeployment-issues
fix: Portainer redeployment fails when Telegram env vars are absent
2026-03-04 14:30:44 +01:00
copilot-swe-agent[bot]
2e1142dba5 fix: resolve Portainer redeployment issues
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 13:26:13 +00:00
copilot-swe-agent[bot]
13fa559f51 Initial plan 2026-03-04 13:16:37 +00:00
xCyanGrizzly
5079e65783 Merge pull request #1 from xCyanGrizzly/copilot/fix-docker-compose-error
fix: copy prisma CLI binary into production Docker image
2026-03-04 14:11:32 +01:00
13 changed files with 98 additions and 88 deletions

View File

@@ -40,12 +40,13 @@ COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Copy node_modules for prisma CLI (needed for migrate deploy at startup)
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
COPY --from=builder /app/node_modules/prisma ./node_modules/prisma
COPY --from=builder /app/node_modules/.bin/prisma ./node_modules/.bin/prisma
COPY --from=builder /app/node_modules/dotenv ./node_modules/dotenv
# Copy node_modules for prisma CLI (needed for migrate deploy at startup).
# Copying the full directory ensures all transitive dependencies are present.
COPY --from=builder /app/node_modules ./node_modules
# Recreate the .bin/prisma symlink so Node resolves __dirname to prisma/build/,
# where the WASM files live (COPY dereferences symlinks, breaking WASM resolution)
RUN mkdir -p ./node_modules/.bin && \
ln -sf ../prisma/build/index.js ./node_modules/.bin/prisma
# Copy entrypoint script
COPY --chown=nextjs:nodejs docker-entrypoint.sh ./

View File

@@ -3,6 +3,7 @@ services:
build:
context: .
dockerfile: Dockerfile
pull_policy: never
ports:
- "${APP_PORT:-3000}:3000"
environment:
@@ -34,10 +35,11 @@ services:
build:
context: .
dockerfile: worker/Dockerfile
pull_policy: never
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER:-dragons}:${POSTGRES_PASSWORD:-stash}@db:5432/${POSTGRES_DB:-dragonsstash}
- TELEGRAM_API_ID=${TELEGRAM_API_ID:?Set TELEGRAM_API_ID in .env}
- TELEGRAM_API_HASH=${TELEGRAM_API_HASH:?Set TELEGRAM_API_HASH in .env}
- TELEGRAM_API_ID=${TELEGRAM_API_ID:-}
- TELEGRAM_API_HASH=${TELEGRAM_API_HASH:-}
- WORKER_INTERVAL_MINUTES=${WORKER_INTERVAL_MINUTES:-60}
- WORKER_TEMP_DIR=/tmp/zips
- TDLIB_STATE_DIR=/data/tdlib
@@ -65,11 +67,12 @@ services:
build:
context: .
dockerfile: bot/Dockerfile
pull_policy: never
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER:-dragons}:${POSTGRES_PASSWORD:-stash}@db:5432/${POSTGRES_DB:-dragonsstash}
- BOT_TOKEN=${BOT_TOKEN:?Set BOT_TOKEN in .env}
- TELEGRAM_API_ID=${TELEGRAM_API_ID:?Set TELEGRAM_API_ID in .env}
- TELEGRAM_API_HASH=${TELEGRAM_API_HASH:?Set TELEGRAM_API_HASH in .env}
- BOT_TOKEN=${BOT_TOKEN:-}
- TELEGRAM_API_ID=${TELEGRAM_API_ID:-}
- TELEGRAM_API_HASH=${TELEGRAM_API_HASH:-}
- LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
- tdlib_bot_state:/data/tdlib

View File

@@ -10,11 +10,11 @@ if [ "$AUTH_SECRET" = "change-me-to-a-random-secret-in-production" ] || [ -z "$A
fi
echo "Running database migrations..."
npx prisma migrate deploy
./node_modules/.bin/prisma migrate deploy
if [ "$SEED_DATABASE" = "true" ]; then
echo "Seeding database..."
npx prisma db seed || echo "Seeding skipped or already done."
./node_modules/.bin/prisma db seed || echo "Seeding skipped or already done."
fi
echo "Starting application..."

10
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "^17.3.1",
"geist": "^1.7.0",
"lucide-react": "^0.574.0",
"next": "16.1.6",
"next-auth": "^5.0.0-beta.30",
@@ -8100,6 +8101,15 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/geist": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/geist/-/geist-1.7.0.tgz",
"integrity": "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==",
"license": "SIL OPEN FONT LICENSE",
"peerDependencies": {
"next": ">=13.2.0"
}
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",

View File

@@ -28,6 +28,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dotenv": "^17.3.1",
"geist": "^1.7.0",
"lucide-react": "^0.574.0",
"next": "16.1.6",
"next-auth": "^5.0.0-beta.30",

View File

@@ -21,12 +21,17 @@ export async function registerUser(input: unknown): Promise<ActionResult<{ id: s
const hashedPassword = await bcrypt.hash(parsed.data.password, 10);
const user = await prisma.user.create({
// First user to register becomes ADMIN (self-hosted owner)
const user = await prisma.$transaction(async (tx) => {
const userCount = await tx.user.count();
const role = userCount === 0 ? "ADMIN" : "USER";
return tx.user.create({
data: {
name: parsed.data.name,
email: parsed.data.email,
hashedPassword,
role: "USER",
role,
settings: {
create: {
lowStockThreshold: 10,
@@ -37,6 +42,7 @@ export async function registerUser(input: unknown): Promise<ActionResult<{ id: s
},
},
});
});
return { success: true, data: { id: user.id } };
}

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { SessionProvider } from "@/components/providers/session-provider";
import { ThemeProvider } from "@/components/providers/theme-provider";
import { Toaster } from "@/components/ui/sonner";
@@ -7,16 +8,6 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { APP_NAME } from "@/lib/constants";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: {
default: APP_NAME,
@@ -29,7 +20,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark" suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}>
<body className={`${GeistSans.variable} ${GeistMono.variable} font-sans antialiased`}>
<SessionProvider>
<ThemeProvider>
<TooltipProvider delayDuration={0}>

View File

@@ -2,6 +2,7 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import {
LayoutDashboard,
Cylinder,
@@ -17,27 +18,17 @@ import {
Flame,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { APP_NAME } from "@/lib/constants";
import { APP_NAME, NAV_ITEMS } from "@/lib/constants";
import { SheetHeader, SheetTitle } from "@/components/ui/sheet";
const icons = { LayoutDashboard, Cylinder, Droplets, Paintbrush, Gem, FileBox, Send, ClipboardList, Building2, MapPin, Settings };
const navItems = [
{ label: "Dashboard", href: "/dashboard", icon: "LayoutDashboard" as const },
{ label: "Filaments", href: "/filaments", icon: "Cylinder" as const },
{ label: "Resins", href: "/resins", icon: "Droplets" as const },
{ label: "Paints", href: "/paints", icon: "Paintbrush" as const },
{ label: "Supplies", href: "/supplies", icon: "Gem" as const },
{ label: "STL Files", href: "/stls", icon: "FileBox" as const },
{ label: "Telegram", href: "/telegram", icon: "Send" as const },
{ label: "Usage", href: "/usage", icon: "ClipboardList" as const },
{ label: "Vendors", href: "/vendors", icon: "Building2" as const },
{ label: "Locations", href: "/locations", icon: "MapPin" as const },
{ label: "Settings", href: "/settings", icon: "Settings" as const },
];
export function MobileSidebar() {
const pathname = usePathname();
const { data: session } = useSession();
const isAdmin = session?.user?.role === "ADMIN";
const visibleItems = NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin);
return (
<div className="flex h-full flex-col">
@@ -48,7 +39,7 @@ export function MobileSidebar() {
</SheetTitle>
</SheetHeader>
<nav className="flex-1 space-y-1 p-2">
{navItems.map((item) => {
{visibleItems.map((item) => {
const Icon = icons[item.icon];
const isActive = pathname.startsWith(item.href);

View File

@@ -3,6 +3,7 @@
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import {
LayoutDashboard,
Cylinder,
@@ -20,7 +21,7 @@ import {
PanelLeft,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { APP_NAME } from "@/lib/constants";
import { APP_NAME, NAV_ITEMS } from "@/lib/constants";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@@ -38,23 +39,13 @@ const icons = {
Settings,
} as const;
const navItems = [
{ label: "Dashboard", href: "/dashboard", icon: "LayoutDashboard" as const },
{ label: "Filaments", href: "/filaments", icon: "Cylinder" as const },
{ label: "Resins", href: "/resins", icon: "Droplets" as const },
{ label: "Paints", href: "/paints", icon: "Paintbrush" as const },
{ label: "Supplies", href: "/supplies", icon: "Gem" as const },
{ label: "STL Files", href: "/stls", icon: "FileBox" as const },
{ label: "Telegram", href: "/telegram", icon: "Send" as const },
{ label: "Usage", href: "/usage", icon: "ClipboardList" as const },
{ label: "Vendors", href: "/vendors", icon: "Building2" as const },
{ label: "Locations", href: "/locations", icon: "MapPin" as const },
{ label: "Settings", href: "/settings", icon: "Settings" as const },
];
export function Sidebar() {
const pathname = usePathname();
const [collapsed, setCollapsed] = useState(false);
const { data: session } = useSession();
const isAdmin = session?.user?.role === "ADMIN";
const visibleItems = NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin);
return (
<aside
@@ -73,7 +64,7 @@ export function Sidebar() {
{/* Navigation */}
<nav className="flex-1 space-y-1 p-2">
{navItems.map((item) => {
{visibleItems.map((item) => {
const Icon = icons[item.icon];
const isActive = pathname.startsWith(item.href);

View File

@@ -18,7 +18,12 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
async jwt({ token, user }) {
if (user) {
token.id = user.id!;
token.role = user.role ?? "USER";
// Fetch the role from the database to pick up first-user ADMIN promotion
const dbUser = await prisma.user.findUnique({
where: { id: user.id! },
select: { role: true },
});
token.role = dbUser?.role ?? user.role ?? "USER";
}
return token;
},
@@ -33,6 +38,18 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
events: {
async createUser({ user }) {
if (user.id) {
// First user to register becomes ADMIN (self-hosted owner)
const adminExists = await prisma.user.findFirst({
where: { role: "ADMIN" },
select: { id: true },
});
if (!adminExists) {
await prisma.user.update({
where: { id: user.id },
data: { role: "ADMIN" },
});
}
await prisma.userSettings.upsert({
where: { userId: user.id },
update: {},

View File

@@ -1,17 +1,17 @@
export const APP_NAME = "Dragon's Stash";
export const NAV_ITEMS = [
{ label: "Dashboard", href: "/dashboard", icon: "LayoutDashboard" },
{ label: "Filaments", href: "/filaments", icon: "Cylinder" },
{ label: "Resins", href: "/resins", icon: "Droplets" },
{ label: "Paints", href: "/paints", icon: "Paintbrush" },
{ label: "Supplies", href: "/supplies", icon: "Gem" },
{ label: "STL Files", href: "/stls", icon: "FileBox" },
{ label: "Telegram", href: "/telegram", icon: "Send" },
{ label: "Usage", href: "/usage", icon: "ClipboardList" },
{ label: "Vendors", href: "/vendors", icon: "Building2" },
{ label: "Locations", href: "/locations", icon: "MapPin" },
{ label: "Settings", href: "/settings", icon: "Settings" },
{ label: "Dashboard", href: "/dashboard", icon: "LayoutDashboard", adminOnly: false },
{ label: "Filaments", href: "/filaments", icon: "Cylinder", adminOnly: false },
{ label: "Resins", href: "/resins", icon: "Droplets", adminOnly: false },
{ label: "Paints", href: "/paints", icon: "Paintbrush", adminOnly: false },
{ label: "Supplies", href: "/supplies", icon: "Gem", adminOnly: false },
{ label: "STL Files", href: "/stls", icon: "FileBox", adminOnly: false },
{ label: "Telegram", href: "/telegram", icon: "Send", adminOnly: true },
{ label: "Usage", href: "/usage", icon: "ClipboardList", adminOnly: false },
{ label: "Vendors", href: "/vendors", icon: "Building2", adminOnly: false },
{ label: "Locations", href: "/locations", icon: "MapPin", adminOnly: false },
{ label: "Settings", href: "/settings", icon: "Settings", adminOnly: false },
] as const;
export const MATERIALS = [

View File

@@ -1,6 +0,0 @@
import { Inter } from "next/font/google";
export const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});

View File

@@ -13,6 +13,11 @@ async function main(): Promise<void> {
log.info("DragonsStash Telegram Worker starting");
log.info({ config: { ...config, databaseUrl: "***" } }, "Configuration loaded");
if (!config.telegramApiId || !config.telegramApiHash) {
log.fatal("TELEGRAM_API_ID and TELEGRAM_API_HASH are both required");
process.exit(1);
}
// Ensure temp directory exists
await mkdir(config.tempDir, { recursive: true });
await mkdir(config.tdlibStateDir, { recursive: true });