11 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
6 changed files with 66 additions and 63 deletions

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import { import {
LayoutDashboard, LayoutDashboard,
Cylinder, Cylinder,
@@ -17,27 +18,17 @@ import {
Flame, Flame,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; 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"; import { SheetHeader, SheetTitle } from "@/components/ui/sheet";
const icons = { LayoutDashboard, Cylinder, Droplets, Paintbrush, Gem, FileBox, Send, ClipboardList, Building2, MapPin, Settings }; 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() { export function MobileSidebar() {
const pathname = usePathname(); const pathname = usePathname();
const { data: session } = useSession();
const isAdmin = session?.user?.role === "ADMIN";
const visibleItems = NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin);
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
@@ -48,7 +39,7 @@ export function MobileSidebar() {
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
<nav className="flex-1 space-y-1 p-2"> <nav className="flex-1 space-y-1 p-2">
{navItems.map((item) => { {visibleItems.map((item) => {
const Icon = icons[item.icon]; const Icon = icons[item.icon];
const isActive = pathname.startsWith(item.href); const isActive = pathname.startsWith(item.href);

View File

@@ -3,6 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import { import {
LayoutDashboard, LayoutDashboard,
Cylinder, Cylinder,
@@ -20,7 +21,7 @@ import {
PanelLeft, PanelLeft,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; 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 { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@@ -38,23 +39,13 @@ const icons = {
Settings, Settings,
} as const; } 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() { export function Sidebar() {
const pathname = usePathname(); const pathname = usePathname();
const [collapsed, setCollapsed] = useState(false); 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 ( return (
<aside <aside
@@ -73,7 +64,7 @@ export function Sidebar() {
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 space-y-1 p-2"> <nav className="flex-1 space-y-1 p-2">
{navItems.map((item) => { {visibleItems.map((item) => {
const Icon = icons[item.icon]; const Icon = icons[item.icon];
const isActive = pathname.startsWith(item.href); const isActive = pathname.startsWith(item.href);

View File

@@ -18,7 +18,12 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
async jwt({ token, user }) { async jwt({ token, user }) {
if (user) { if (user) {
token.id = user.id!; 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; return token;
}, },
@@ -33,6 +38,18 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
events: { events: {
async createUser({ user }) { async createUser({ user }) {
if (user.id) { 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({ await prisma.userSettings.upsert({
where: { userId: user.id }, where: { userId: user.id },
update: {}, update: {},

View File

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