13 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
987167de0c Enable worker service by default in docker-compose
Remove profiles from worker service in both docker-compose.yml and
docker-compose.dev.yml so the worker starts automatically with
`docker compose up`. This fixes the issue where verification SMS and
the scheduler timer were not working because the worker was never
started. The bot remains as an optional profile.

Update README to reflect the change.

Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 21:13:00 +00:00
copilot-swe-agent[bot]
4f331d5411 Initial plan 2026-03-04 21:09:51 +00:00
xCyanGrizzly
8088a86feb Merge pull request #9 from xCyanGrizzly/copilot/fix-admin-access-issue
Make all users admins in self-hosted deployment
2026-03-04 21:58:04 +01:00
copilot-swe-agent[bot]
b53934ebf2 Make all users admins: update schema default, add migration, simplify registration and OAuth flows
Co-authored-by: xCyanGrizzly <53275238+xCyanGrizzly@users.noreply.github.com>
2026-03-04 20:23:54 +00:00
copilot-swe-agent[bot]
464c86b32a Initial plan 2026-03-04 20:16:22 +00:00
xCyanGrizzly
fc00fb6f2e Merge pull request #8 from xCyanGrizzly/copilot/fix-admin-account-login
Fix first user not getting ADMIN role when signing up via OAuth
2026-03-04 20:24:38 +01:00
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
10 changed files with 48 additions and 54 deletions

View File

@@ -125,18 +125,15 @@ docker compose up -d
The app will be available at [http://localhost:3000](http://localhost:3000). The app will be available at [http://localhost:3000](http://localhost:3000).
### Adding Telegram Services ### Adding the Telegram Bot
The worker and bot run as optional profiles so `docker compose up` works with just the app + database: The worker starts by default with `docker compose up`. The bot runs as an optional profile:
```bash ```bash
# App + DB + Telegram worker (needs TELEGRAM_API_ID + TELEGRAM_API_HASH in .env)
docker compose --profile telegram up -d
# App + DB + Worker + Bot (also needs BOT_TOKEN in .env) # App + DB + Worker + Bot (also needs BOT_TOKEN in .env)
docker compose --profile full up -d docker compose --profile full up -d
# Or just the bot (alongside app + db) # Or just the bot (alongside app + db + worker)
docker compose --profile bot up -d docker compose --profile bot up -d
``` ```

View File

@@ -16,7 +16,6 @@ services:
retries: 5 retries: 5
worker: worker:
profiles: ["telegram", "full"]
build: build:
context: . context: .
dockerfile: worker/Dockerfile dockerfile: worker/Dockerfile

View File

@@ -31,7 +31,6 @@ services:
- frontend - frontend
worker: worker:
profiles: ["telegram", "full"]
build: build:
context: . context: .
dockerfile: worker/Dockerfile dockerfile: worker/Dockerfile

View File

@@ -0,0 +1,5 @@
-- Promote all existing users to ADMIN (self-hosted: every user is an admin)
UPDATE "User" SET "role" = 'ADMIN' WHERE "role" = 'USER';
-- Change the default role for new users to ADMIN
ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'ADMIN';

View File

@@ -22,7 +22,7 @@ model User {
emailVerified DateTime? emailVerified DateTime?
image String? image String?
hashedPassword String? hashedPassword String?
role Role @default(USER) role Role @default(ADMIN)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@@ -21,12 +21,13 @@ 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);
// Self-hosted: all users are admins
const user = await prisma.user.create({ const user = await prisma.user.create({
data: { data: {
name: parsed.data.name, name: parsed.data.name,
email: parsed.data.email, email: parsed.data.email,
hashedPassword, hashedPassword,
role: "USER", role: "ADMIN",
settings: { settings: {
create: { create: {
lowStockThreshold: 10, lowStockThreshold: 10,

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 ensure token reflects current role
const dbUser = await prisma.user.findUnique({
where: { id: user.id! },
select: { role: true },
});
token.role = dbUser?.role ?? user.role ?? "ADMIN";
} }
return token; return token;
}, },
@@ -33,6 +38,12 @@ export const { auth, handlers, signIn, signOut } = NextAuth({
events: { events: {
async createUser({ user }) { async createUser({ user }) {
if (user.id) { if (user.id) {
// Self-hosted: all users are admins
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 = [