5 Commits

11 changed files with 41 additions and 66 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.git
.env
*.log
data/*
!data/branding/
!data/branding/**
frontend/node_modules/
frontend/.next/
backend/__pycache__/
**/__pycache__/
**/*.pyc

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
.env .env
.venv/ .venv/
data/ data/
!data/branding/
!data/branding/**
backend/__pycache__/ backend/__pycache__/
**/__pycache__/ **/__pycache__/
*.pyc *.pyc

View File

@@ -5,10 +5,11 @@ WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1
COPY requirements.txt . COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app COPY backend/app ./app
COPY data/branding /app/data/branding
EXPOSE 8000 EXPOSE 8000

BIN
data/branding/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
data/branding/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

19
docker-compose.hub.yml Normal file
View File

@@ -0,0 +1,19 @@
services:
backend:
image: rephl3xnz/magent-backend:latest
env_file:
- ./.env
ports:
- "8000:8000"
volumes:
- ./data:/app/data
frontend:
image: rephl3xnz/magent-frontend:latest
environment:
- NEXT_PUBLIC_API_BASE=/api
- BACKEND_INTERNAL_URL=http://backend:8000
ports:
- "3000:3000"
depends_on:
- backend

View File

@@ -1,8 +1,8 @@
services: services:
backend: backend:
build: build:
context: ./backend context: .
dockerfile: Dockerfile dockerfile: backend/Dockerfile
env_file: env_file:
- ./.env - ./.env
ports: ports:

View File

@@ -8,6 +8,7 @@ COPY package.json ./
RUN npm install RUN npm install
COPY app ./app COPY app ./app
COPY public ./public
COPY next-env.d.ts ./next-env.d.ts COPY next-env.d.ts ./next-env.d.ts
COPY next.config.js ./next.config.js COPY next.config.js ./next.config.js
COPY tsconfig.json ./tsconfig.json COPY tsconfig.json ./tsconfig.json
@@ -22,6 +23,7 @@ ENV NEXT_TELEMETRY_DISABLED=1 \
NODE_ENV=production NODE_ENV=production
COPY --from=builder /app/.next ./.next COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/next.config.js ./next.config.js COPY --from=builder /app/next.config.js ./next.config.js

View File

@@ -4,12 +4,11 @@ import { useEffect } from 'react'
export default function BrandingFavicon() { export default function BrandingFavicon() {
useEffect(() => { useEffect(() => {
const href = '/branding-icon.svg' const href = '/api/branding/favicon.ico'
let link = document.querySelector("link[rel='icon']") as HTMLLinkElement | null let link = document.querySelector("link[rel='icon']") as HTMLLinkElement | null
if (!link) { if (!link) {
link = document.createElement('link') link = document.createElement('link')
link.rel = 'icon' link.rel = 'icon'
link.type = 'image/svg+xml'
document.head.appendChild(link) document.head.appendChild(link)
} }
link.href = href link.href = href

View File

@@ -7,7 +7,7 @@ export default function BrandingLogo({ className, alt = 'Magent logo' }: Brandin
return ( return (
<img <img
className={className} className={className}
src="/branding-logo.svg" src="/api/branding/logo.png"
alt={alt} alt={alt}
/> />
) )

View File

@@ -25,8 +25,6 @@ export default function UsersPage() {
const [users, setUsers] = useState<AdminUser[]>([]) const [users, setUsers] = useState<AdminUser[]>([])
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [passwordInputs, setPasswordInputs] = useState<Record<string, string>>({})
const [passwordStatus, setPasswordStatus] = useState<Record<string, string>>({})
const loadUsers = async () => { const loadUsers = async () => {
try { try {
@@ -105,42 +103,6 @@ export default function UsersPage() {
} }
} }
const updateUserPassword = async (username: string) => {
const newPassword = passwordInputs[username] || ''
if (!newPassword || newPassword.length < 8) {
setPasswordStatus((current) => ({
...current,
[username]: 'Password must be at least 8 characters.',
}))
return
}
try {
const baseUrl = getApiBase()
const response = await authFetch(
`${baseUrl}/admin/users/${encodeURIComponent(username)}/password`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: newPassword }),
}
)
if (!response.ok) {
const text = await response.text()
throw new Error(text || 'Update failed')
}
setPasswordInputs((current) => ({ ...current, [username]: '' }))
setPasswordStatus((current) => ({
...current,
[username]: 'Password updated.',
}))
} catch (err) {
console.error(err)
setPasswordStatus((current) => ({
...current,
[username]: 'Could not update password.',
}))
}
}
useEffect(() => { useEffect(() => {
if (!getToken()) { if (!getToken()) {
@@ -197,27 +159,6 @@ export default function UsersPage() {
{user.isBlocked ? 'Allow access' : 'Block access'} {user.isBlocked ? 'Allow access' : 'Block access'}
</button> </button>
</div> </div>
{user.authProvider === 'local' && (
<div className="user-actions">
<input
type="password"
placeholder="New password (min 8 chars)"
value={passwordInputs[user.username] || ''}
onChange={(event) =>
setPasswordInputs((current) => ({
...current,
[user.username]: event.target.value,
}))
}
/>
<button type="button" onClick={() => updateUserPassword(user.username)}>
Set password
</button>
</div>
)}
{passwordStatus[user.username] && (
<div className="meta">{passwordStatus[user.username]}</div>
)}
</div> </div>
))} ))}
</div> </div>