7 Commits

Author SHA1 Message Date
47777acd74 Docker tweaks and PDF width fix
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 4s
2025-05-06 18:07:52 -04:00
32d98f4bc0 Updates 2025-05-06 14:03:50 -04:00
ef60832bc2 Add CORS for GETs
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 4s
2025-05-01 22:11:06 -04:00
4f782560de downgrade tus server
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 41s
2025-05-01 21:59:27 -04:00
979cf7320e Switch to alpine
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 43s
2025-05-01 21:41:35 -04:00
1f9f11e1be Fix crash (thank you gpt o3!)
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 5s
2025-05-01 21:18:38 -04:00
1d183c37f8 Cors is gone..? reverting old changes
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 5s
2025-05-01 21:05:40 -04:00
15 changed files with 1609 additions and 1930 deletions

View File

@ -1,30 +0,0 @@
version: "1"
name: Publish Development Docker Image
on:
push:
tags:
- "v[0-9]+\\.[0-9]+\\.[0-9]+-dev"
jobs:
build:
name: Publish Development Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Registry
run: echo "${{ secrets.ACCESS_TOKEN }}" | docker login git.altaiar.dev -u "${{ secrets.USERNAME }}" --password-stdin
- name: Build & Tag Image
run: |
docker build --build-arg APP_VERSION=${{ gitea.ref_name }} -t git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }} .
docker tag git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }} git.altaiar.dev/${{ gitea.repository }}:dev
- name: Push Images
run: |
docker push git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }}
docker push git.altaiar.dev/${{ gitea.repository }}:dev

View File

@ -1,28 +1,36 @@
version: "1" version: "1"
name: Publish Docker Image name: Publish Docker Image
on: on:
push: push:
tags: tags:
- "v[0-9]+\\.[0-9]+\\.[0-9]+" - "*"
jobs: jobs:
build: build:
name: Publish Docker Image name: Publish Docker Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v3
- name: Login to Registry - name: Login to Registry
run: echo "${{ secrets.ACCESS_TOKEN }}" | docker login git.altaiar.dev -u "${{ secrets.USERNAME }}" --password-stdin run: echo "${{ secrets.ACCESS_TOKEN }}" \
| docker login git.altaiar.dev -u "${{ secrets.USERNAME }}" --password-stdin
- name: Build & Tag Image - name: Build & Tag Image
run: | run: |
docker build --build-arg APP_VERSION=${{ gitea.ref_name }} -t git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }} . docker build \
docker tag git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }} git.altaiar.dev/${{ gitea.repository }}:latest --build-arg APP_VERSION=${{ gitea.ref_name }} \
--label org.opencontainers.image.version=${{ gitea.ref_name }} \
-t git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }} .
docker tag \
git.altaiar.dev/${{ gitea.repository }}:${{ gitea.ref_name }} \
git.altaiar.dev/${{ gitea.repository }}:latest
- name: Push Images - name: Push Images
run: | run: |

View File

@ -1,15 +1,9 @@
# base FROM node:lts-alpine AS base
# ----
FROM node:20-bookworm-slim AS base
RUN corepack enable ARG APP_VERSION=dev
ENV APP_VERSION=${APP_VERSION}
# We tried to make the Dockerfile as lean as possible. In some cases, that means we excluded a dependency your project needs. RUN apk add --no-cache openssl && corepack enable
# By far the most common is Python. If you're running into build errors because `python3` isn't available,
# add `python3 make gcc \` before the `openssl \` line below and in other stages as necessary:
RUN apt-get update && apt-get install -y \
openssl \
&& rm -rf /var/lib/apt/lists/*
USER node USER node
WORKDIR /home/node/app WORKDIR /home/node/app
@ -30,13 +24,8 @@ RUN --mount=type=cache,target=/home/node/.yarn/berry/cache,uid=1000 \
COPY --chown=node:node redwood.toml . COPY --chown=node:node redwood.toml .
COPY --chown=node:node graphql.config.js . COPY --chown=node:node graphql.config.js .
# api build
# ---------
FROM base AS api_build FROM base AS api_build
# If your api side build relies on build-time environment variables,
# specify them here as ARGs. (But don't put secrets in your Dockerfile!)
ARG ADDRESS_PROD ARG ADDRESS_PROD
ARG ADDRESS_DEV ARG ADDRESS_DEV
ARG DOMAIN ARG DOMAIN
@ -56,8 +45,6 @@ ARG APP_VERSION
COPY --chown=node:node api api COPY --chown=node:node api api
RUN yarn rw build api RUN yarn rw build api
# web prerender build
# -------------------
FROM api_build AS web_build_with_prerender FROM api_build AS web_build_with_prerender
ARG FIRST_NAME ARG FIRST_NAME
@ -70,13 +57,11 @@ ARG API_ADDRESS_PROD
ARG API_ADDRESS_DEV ARG API_ADDRESS_DEV
ARG APP_VERSION ARG APP_VERSION
ENV APP_VERSION=$APP_VERSION ENV APP_VERSION=${APP_VERSION}
COPY --chown=node:node web web COPY --chown=node:node web web
RUN yarn rw build web RUN yarn rw build web
# web build
# ---------
FROM base AS web_build FROM base AS web_build
ARG FIRST_NAME ARG FIRST_NAME
@ -89,20 +74,17 @@ ARG API_ADDRESS_PROD
ARG API_ADDRESS_DEV ARG API_ADDRESS_DEV
ARG APP_VERSION ARG APP_VERSION
ENV APP_VERSION=$APP_VERSION ENV APP_VERSION=${APP_VERSION}
COPY --chown=node:node web web COPY --chown=node:node web web
RUN yarn rw build web --no-prerender RUN yarn rw build web --no-prerender
# api serve FROM node:lts-alpine AS api_serve
# ---------
FROM node:20-bookworm-slim AS api_serve
RUN corepack enable RUN apk add --no-cache openssl && corepack enable
RUN apt-get update && apt-get install -y \ RUN mkdir -p /home/node/app/api/files_prod \
openssl \ && chown -R node:node /home/node/app/api/files_prod
&& rm -rf /var/lib/apt/lists/*
USER node USER node
WORKDIR /home/node/app WORKDIR /home/node/app
@ -129,20 +111,11 @@ COPY --chown=node:node --from=api_build /home/node/app/node_modules/.prisma /hom
ARG APP_VERSION ARG APP_VERSION
ENV NODE_ENV=production ENV NODE_ENV=production
ENV APP_VERSION=$APP_VERSION ENV APP_VERSION=${APP_VERSION}
# default api serve command
# ---------
# If you are using a custom server file, you must use the following
# command to launch your server instead of the default api-server below.
# This is important if you intend to configure GraphQL to use Realtime.
CMD [ "./api/dist/server.js" ] CMD [ "./api/dist/server.js" ]
# CMD [ "node_modules/.bin/rw-server", "api" ]
# web serve FROM node:lts-alpine AS web_serve
# ---------
FROM node:20-bookworm-slim AS web_serve
RUN corepack enable RUN corepack enable
@ -167,29 +140,15 @@ COPY --chown=node:node graphql.config.js .
COPY --chown=node:node --from=web_build /home/node/app/web/dist /home/node/app/web/dist COPY --chown=node:node --from=web_build /home/node/app/web/dist /home/node/app/web/dist
ARG APP_VERSION ARG APP_VERSION
ENV APP_VERSION=$APP_VERSION ENV APP_VERSION=${APP_VERSION}
ENV NODE_ENV=production \ ENV NODE_ENV=production \
API_PROXY_TARGET=http://api:8911 API_PROXY_TARGET=http://api:8911
# We use the shell form here for variable expansion.
CMD "node_modules/.bin/rw-web-server" "--api-proxy-target" "$API_PROXY_TARGET" CMD "node_modules/.bin/rw-web-server" "--api-proxy-target" "$API_PROXY_TARGET"
# console
# -------
FROM base AS console FROM base AS console
# To add more packages:
#
# ```
# USER root
#
# RUN apt-get update && apt-get install -y \
# curl
#
# USER node
# ```
COPY --chown=node:node api api COPY --chown=node:node api api
COPY --chown=node:node web web COPY --chown=node:node web web
COPY --chown=node:node scripts scripts COPY --chown=node:node scripts scripts

View File

@ -76,11 +76,6 @@ volumes:
postgres: postgres:
files: # For persistent file storage across upgrades files: # For persistent file storage across upgrades
``` ```
## Fix Files Ownership
The `files` volume in Docker is owned by `root`, since the portfolio container runs under the `node` user, file uploads will fail. Run this command to give ownership to the `node` user:
```
sudo docker exec -u root portfolio chown -R node:node /home/node/app/api/files_prod
```
## Logging In ## Logging In
- Once the container is up and running, head to `/login` (`https://portfolio.example.com/login`), default credentials are below - Once the container is up and running, head to `/login` (`https://portfolio.example.com/login`), default credentials are below
- If you would like to change the password, head to `/forgot-password` (`https://portfolio.example.com/forgot-password`), the username is `admin` - If you would like to change the password, head to `/forgot-password` (`https://portfolio.example.com/forgot-password`), the username is `admin`

View File

@ -5,12 +5,12 @@
"dependencies": { "dependencies": {
"@fastify/cors": "^9.0.1", "@fastify/cors": "^9.0.1",
"@fastify/rate-limit": "^9.1.0", "@fastify/rate-limit": "^9.1.0",
"@redwoodjs/api": "8.4.0", "@redwoodjs/api": "8.6.1",
"@redwoodjs/api-server": "8.4.0", "@redwoodjs/api-server": "8.6.1",
"@redwoodjs/auth-dbauth-api": "8.4.0", "@redwoodjs/auth-dbauth-api": "8.6.1",
"@redwoodjs/graphql-server": "8.4.0", "@redwoodjs/graphql-server": "8.6.1",
"@tus/file-store": "^2.0.0", "@tus/file-store": "1.4.0",
"@tus/server": "^2.0.0", "@tus/server": "1.7.0",
"countries-list": "^3.1.1", "countries-list": "^3.1.1",
"graphql-scalars": "^1.23.0", "graphql-scalars": "^1.23.0",
"nodemailer": "^6.9.14" "nodemailer": "^6.9.14"

View File

@ -2,10 +2,17 @@ import type { FastifyReply } from 'fastify'
import { isProduction } from '@redwoodjs/api/logger' import { isProduction } from '@redwoodjs/api/logger'
export const setCorsHeaders = (res: FastifyReply) => { export const setCorsHeaders = (
res: FastifyReply,
isPublic: boolean = false
) => {
res.raw.setHeader( res.raw.setHeader(
'Access-Control-Allow-Origin', 'Access-Control-Allow-Origin',
isProduction ? process.env.ADDRESS_PROD : process.env.ADDRESS_DEV isPublic
? '*'
: isProduction
? process.env.ADDRESS_PROD
: process.env.ADDRESS_DEV
) )
res.raw.setHeader( res.raw.setHeader(
'Access-Control-Allow-Methods', 'Access-Control-Allow-Methods',
@ -16,4 +23,9 @@ export const setCorsHeaders = (res: FastifyReply) => {
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset' 'Origin, X-Requested-With, Content-Type, Accept, Authorization, Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset'
) )
res.raw.setHeader('Access-Control-Allow-Credentials', 'true') res.raw.setHeader('Access-Control-Allow-Credentials', 'true')
res.raw.setHeader(
'Access-Control-Expose-Headers',
'Upload-Offset, Upload-Length, Upload-Metadata, Tus-Version,' +
'Tus-Resumable, Tus-Max-Size, Tus-Extension, Tus-Checksum-Algorithm'
)
} }

View File

@ -19,16 +19,22 @@ interface User {
resetTokenExpiresAt: Date | null resetTokenExpiresAt: Date | null
} }
export const handleTusUpload = async ( export const handleTusUpload = (
req: FastifyRequest, req: FastifyRequest,
res: FastifyReply, res: FastifyReply,
tusHandler: Server, tusHandler: Server,
isPublicEndpoint: boolean isPublicEndpoint: boolean
) => { ) => {
res.hijack()
if (req.method === 'GET' && isPublicEndpoint) {
setCorsHeaders(res)
}
if (isProduction) { if (isProduction) {
if (req.method === 'OPTIONS') handleOptionsRequest(res) if (req.method === 'OPTIONS') handleOptionsRequest(res)
else if (isPublicEndpoint && req.method === 'GET') else if (isPublicEndpoint && req.method === 'GET')
await tusHandler.handle(req.raw, res.raw) void tusHandler.handle(req.raw, res.raw)
else if (['GET', 'POST', 'HEAD', 'PATCH'].includes(req.method)) { else if (['GET', 'POST', 'HEAD', 'PATCH'].includes(req.method)) {
if (req.headers.cookie) handleAuthenticatedRequest(req, res, tusHandler) if (req.headers.cookie) handleAuthenticatedRequest(req, res, tusHandler)
else { else {
@ -40,8 +46,8 @@ export const handleTusUpload = async (
res.raw.end('Method not allowed') res.raw.end('Method not allowed')
} }
} else { } else {
setCorsHeaders(res) setCorsHeaders(res, isPublicEndpoint)
await tusHandler.handle(req.raw, res.raw) void tusHandler.handle(req.raw, res.raw)
} }
} }

View File

@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import Cors from '@fastify/cors' import Cors from '@fastify/cors'
import RateLimit from '@fastify/rate-limit' import RateLimit from '@fastify/rate-limit'
import { FileStore } from '@tus/file-store'
import { Server } from '@tus/server'
import { isProduction } from '@redwoodjs/api/logger' import { isProduction } from '@redwoodjs/api/logger'
import { createServer } from '@redwoodjs/api-server' import { createServer } from '@redwoodjs/api-server'
@ -15,8 +17,6 @@ enum Theme {
;(async () => { ;(async () => {
const { countries } = await import('countries-list') const { countries } = await import('countries-list')
const { FileStore } = await import('@tus/file-store')
const { Server } = await import('@tus/server')
if (!Object.keys(countries).includes(process.env.COUNTRY)) if (!Object.keys(countries).includes(process.env.COUNTRY))
throw new Error( throw new Error(
@ -65,12 +65,14 @@ enum Theme {
(_request, _payload, done) => done(null) (_request, _payload, done) => done(null)
) )
server.all('/files', (req, res) => server.all('/files', (req, res) => {
res.hijack()
handleTusUpload(req, res, tusServer, false) handleTusUpload(req, res, tusServer, false)
) })
server.all('/files/*', (req, res) => server.all('/files/*', (req, res) => {
res.hijack()
handleTusUpload(req, res, tusServer, true) handleTusUpload(req, res, tusServer, true)
) })
await server.start() await server.start()
})() })()

View File

@ -4,8 +4,8 @@
"allowJs": true, "allowJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"target": "ES2023", "target": "ES2023",
"module": "NodeNext", "module": "Node16",
"moduleResolution": "nodenext", "moduleResolution": "node16",
"skipLibCheck": false, "skipLibCheck": false,
"rootDirs": [ "rootDirs": [
"./src", "./src",

View File

@ -7,9 +7,9 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@redwoodjs/auth-dbauth-setup": "8.4.0", "@redwoodjs/auth-dbauth-setup": "8.6.1",
"@redwoodjs/core": "8.4.0", "@redwoodjs/core": "8.6.1",
"@redwoodjs/project-config": "8.4.0", "@redwoodjs/project-config": "8.6.1",
"prettier-plugin-tailwindcss": "0.4.1" "prettier-plugin-tailwindcss": "0.4.1"
}, },
"eslintConfig": { "eslintConfig": {

View File

@ -14,11 +14,11 @@
"@icons-pack/react-simple-icons": "^10.0.0", "@icons-pack/react-simple-icons": "^10.0.0",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@redwoodjs/auth-dbauth-web": "8.4.0", "@redwoodjs/auth-dbauth-web": "8.6.1",
"@redwoodjs/forms": "8.4.0", "@redwoodjs/forms": "8.6.1",
"@redwoodjs/router": "8.4.0", "@redwoodjs/router": "8.6.1",
"@redwoodjs/web": "8.4.0", "@redwoodjs/web": "8.6.1",
"@redwoodjs/web-server": "8.4.0", "@redwoodjs/web-server": "8.6.1",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.15",
"@tiptap/extension-link": "^2.8.0", "@tiptap/extension-link": "^2.8.0",
"@tiptap/extension-text-style": "^2.8.0", "@tiptap/extension-text-style": "^2.8.0",
@ -45,7 +45,7 @@
"react-pdf": "^9.2.1" "react-pdf": "^9.2.1"
}, },
"devDependencies": { "devDependencies": {
"@redwoodjs/vite": "8.4.0", "@redwoodjs/vite": "8.6.1",
"@types/react": "^18.2.55", "@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19", "@types/react-dom": "^18.2.19",
"@types/react-html-parser": "^2", "@types/react-html-parser": "^2",

View File

@ -1,14 +1,16 @@
import { mdiOpenInNew } from '@mdi/js'; import { useState, useRef, useEffect } from 'react'
import Icon from '@mdi/react';
import { useState } from 'react' import { mdiOpenInNew } from '@mdi/js'
import { Document, Page as PdfPage, pdfjs } from "react-pdf"; import Icon from '@mdi/react'
import "react-pdf/dist/Page/AnnotationLayer.css"; import { Document, Page as PdfPage, pdfjs } from 'react-pdf'
import "react-pdf/dist/Page/TextLayer.css";
import 'react-pdf/dist/Page/AnnotationLayer.css'
import 'react-pdf/dist/Page/TextLayer.css'
pdfjs.GlobalWorkerOptions.workerSrc = new URL( pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs", 'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url import.meta.url
).toString(); ).toString()
interface PDFProps { interface PDFProps {
url: string url: string
@ -16,14 +18,29 @@ interface PDFProps {
} }
const PDF = ({ url, form = false }: PDFProps) => { const PDF = ({ url, form = false }: PDFProps) => {
const [numPages, setNumPages] = useState<number>(0); const [numPages, setNumPages] = useState<number>(0)
function onLoadSuccess({ numPages }: { numPages: number }) { function onLoadSuccess({ numPages }: { numPages: number }) {
setNumPages(numPages); setNumPages(numPages)
} }
const containerRef = useRef<HTMLDivElement>(null)
const [containerWidth, setContainerWidth] = useState<number>(0)
useEffect(() => {
function updateWidth() {
if (containerRef.current) {
setContainerWidth(containerRef.current.clientWidth)
}
}
updateWidth()
window.addEventListener('resize', updateWidth)
return () => window.removeEventListener('resize', updateWidth)
}, [])
return ( return (
<div <div
className="overflow-y-auto flex justify-center" ref={containerRef}
className="overflow-auto flex justify-center"
style={{ style={{
width: 'calc(100vw - 1rem)', width: 'calc(100vw - 1rem)',
height: `calc(100vh - ${form ? '8.5rem' : '6rem'})`, height: `calc(100vh - ${form ? '8.5rem' : '6rem'})`,
@ -39,7 +56,11 @@ const PDF = ({ url, form = false }: PDFProps) => {
</a> </a>
<Document file={url} onLoadSuccess={onLoadSuccess}> <Document file={url} onLoadSuccess={onLoadSuccess}>
{Array.from({ length: numPages }, (_, i) => ( {Array.from({ length: numPages }, (_, i) => (
<PdfPage key={i} pageNumber={i + 1} width={800} /> <PdfPage
key={i}
pageNumber={i + 1}
width={Math.min(containerWidth, 800)}
/>
))} ))}
</Document> </Document>
</div> </div>

View File

@ -53,7 +53,7 @@ const Uploader = ({
onBeforeUpload: (files) => { onBeforeUpload: (files) => {
for (const [key, file] of Object.entries(files)) { for (const [key, file] of Object.entries(files)) {
instance.setFileMeta(key, { instance.setFileMeta(key, {
name: `${new Date().getTime().toString()}.${file.extension}`, name: new Date().getTime().toString(),
type: file.type, type: file.type,
contentType: file.type, contentType: file.type,
}) })
@ -68,7 +68,7 @@ const Uploader = ({
removeFingerprintOnSuccess: true, removeFingerprintOnSuccess: true,
}) })
.use(Compressor, { .use(Compressor, {
mimeType: type === 'image' ? 'image/webp' : 'application/pdf', mimeType: 'image/webp',
}) })
if (type === 'image') if (type === 'image')

View File

@ -76,7 +76,7 @@ const HomePage = () => (
rel="noreferrer" rel="noreferrer"
className="btn btn-square" className="btn btn-square"
> >
{getLogoComponent('github')} {getLogoComponent('gitea')}
</a> </a>
</div> </div>
) : ( ) : (

3288
yarn.lock

File diff suppressed because it is too large Load Diff