Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
47777acd74
|
|||
32d98f4bc0
|
|||
ef60832bc2
|
|||
4f782560de
|
|||
979cf7320e
|
|||
1f9f11e1be
|
|||
1d183c37f8
|
|||
3aeec4d23e
|
|||
debfcf7226
|
|||
15bbc27238
|
|||
16bd44c599
|
|||
d13b16c032
|
|||
d144f7385b
|
@ -10,20 +10,27 @@ jobs:
|
||||
build:
|
||||
name: Publish Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- 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
|
||||
run: |
|
||||
docker build -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
|
||||
docker build \
|
||||
--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
|
||||
run: |
|
||||
|
80
Dockerfile
80
Dockerfile
@ -1,15 +1,9 @@
|
||||
# base
|
||||
# ----
|
||||
FROM node:20-bookworm-slim as base
|
||||
FROM node:lts-alpine 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.
|
||||
# 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/*
|
||||
RUN apk add --no-cache openssl && corepack enable
|
||||
|
||||
USER node
|
||||
WORKDIR /home/node/app
|
||||
@ -30,12 +24,7 @@ RUN --mount=type=cache,target=/home/node/.yarn/berry/cache,uid=1000 \
|
||||
COPY --chown=node:node redwood.toml .
|
||||
COPY --chown=node:node graphql.config.js .
|
||||
|
||||
# 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!)
|
||||
FROM base AS api_build
|
||||
|
||||
ARG ADDRESS_PROD
|
||||
ARG ADDRESS_DEV
|
||||
@ -51,13 +40,12 @@ ARG EMAIL_FROM
|
||||
ARG EMAIL_TO
|
||||
ARG FIRST_NAME
|
||||
ARG LAST_NAME
|
||||
ARG APP_VERSION
|
||||
|
||||
COPY --chown=node:node api 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 LAST_NAME
|
||||
@ -67,13 +55,14 @@ ARG CITY
|
||||
ARG DEFAULT_THEME
|
||||
ARG API_ADDRESS_PROD
|
||||
ARG API_ADDRESS_DEV
|
||||
ARG APP_VERSION
|
||||
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
|
||||
COPY --chown=node:node web web
|
||||
RUN yarn rw build web
|
||||
|
||||
# web build
|
||||
# ---------
|
||||
FROM base as web_build
|
||||
FROM base AS web_build
|
||||
|
||||
ARG FIRST_NAME
|
||||
ARG LAST_NAME
|
||||
@ -83,19 +72,19 @@ ARG CITY
|
||||
ARG DEFAULT_THEME
|
||||
ARG API_ADDRESS_PROD
|
||||
ARG API_ADDRESS_DEV
|
||||
ARG APP_VERSION
|
||||
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
|
||||
COPY --chown=node:node web web
|
||||
RUN yarn rw build web --no-prerender
|
||||
|
||||
# api serve
|
||||
# ---------
|
||||
FROM node:20-bookworm-slim as api_serve
|
||||
FROM node:lts-alpine AS api_serve
|
||||
|
||||
RUN corepack enable
|
||||
RUN apk add --no-cache openssl && corepack enable
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN mkdir -p /home/node/app/api/files_prod \
|
||||
&& chown -R node:node /home/node/app/api/files_prod
|
||||
|
||||
USER node
|
||||
WORKDIR /home/node/app
|
||||
@ -119,20 +108,14 @@ COPY --chown=node:node --from=api_build /home/node/app/api/dist /home/node/app/a
|
||||
COPY --chown=node:node --from=api_build /home/node/app/api/db /home/node/app/api/db
|
||||
COPY --chown=node:node --from=api_build /home/node/app/node_modules/.prisma /home/node/app/node_modules/.prisma
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ARG 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.
|
||||
ENV NODE_ENV=production
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
|
||||
CMD [ "./api/dist/server.js" ]
|
||||
# CMD [ "node_modules/.bin/rw-server", "api" ]
|
||||
|
||||
# web serve
|
||||
# ---------
|
||||
FROM node:20-bookworm-slim as web_serve
|
||||
FROM node:lts-alpine AS web_serve
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
@ -156,26 +139,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
|
||||
|
||||
ARG APP_VERSION
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
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"
|
||||
|
||||
# console
|
||||
# -------
|
||||
FROM base as console
|
||||
|
||||
# To add more packages:
|
||||
#
|
||||
# ```
|
||||
# USER root
|
||||
#
|
||||
# RUN apt-get update && apt-get install -y \
|
||||
# curl
|
||||
#
|
||||
# USER node
|
||||
# ```
|
||||
FROM base AS console
|
||||
|
||||
COPY --chown=node:node api api
|
||||
COPY --chown=node:node web web
|
||||
|
@ -76,11 +76,6 @@ volumes:
|
||||
postgres:
|
||||
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
|
||||
- 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`
|
||||
|
@ -5,12 +5,12 @@
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/rate-limit": "^9.1.0",
|
||||
"@redwoodjs/api": "8.4.0",
|
||||
"@redwoodjs/api-server": "8.4.0",
|
||||
"@redwoodjs/auth-dbauth-api": "8.4.0",
|
||||
"@redwoodjs/graphql-server": "8.4.0",
|
||||
"@tus/file-store": "^1.4.0",
|
||||
"@tus/server": "^1.7.0",
|
||||
"@redwoodjs/api": "8.6.1",
|
||||
"@redwoodjs/api-server": "8.6.1",
|
||||
"@redwoodjs/auth-dbauth-api": "8.6.1",
|
||||
"@redwoodjs/graphql-server": "8.6.1",
|
||||
"@tus/file-store": "1.4.0",
|
||||
"@tus/server": "1.7.0",
|
||||
"countries-list": "^3.1.1",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"nodemailer": "^6.9.14"
|
||||
|
@ -2,10 +2,17 @@ import type { FastifyReply } from 'fastify'
|
||||
|
||||
import { isProduction } from '@redwoodjs/api/logger'
|
||||
|
||||
export const setCorsHeaders = (res: FastifyReply) => {
|
||||
export const setCorsHeaders = (
|
||||
res: FastifyReply,
|
||||
isPublic: boolean = false
|
||||
) => {
|
||||
res.raw.setHeader(
|
||||
'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(
|
||||
'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'
|
||||
)
|
||||
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'
|
||||
)
|
||||
}
|
||||
|
@ -25,10 +25,16 @@ export const handleTusUpload = (
|
||||
tusHandler: Server,
|
||||
isPublicEndpoint: boolean
|
||||
) => {
|
||||
res.hijack()
|
||||
|
||||
if (req.method === 'GET' && isPublicEndpoint) {
|
||||
setCorsHeaders(res)
|
||||
}
|
||||
|
||||
if (isProduction) {
|
||||
if (req.method === 'OPTIONS') handleOptionsRequest(res)
|
||||
else if (isPublicEndpoint && req.method === 'GET')
|
||||
tusHandler.handle(req.raw, res.raw)
|
||||
void tusHandler.handle(req.raw, res.raw)
|
||||
else if (['GET', 'POST', 'HEAD', 'PATCH'].includes(req.method)) {
|
||||
if (req.headers.cookie) handleAuthenticatedRequest(req, res, tusHandler)
|
||||
else {
|
||||
@ -40,8 +46,8 @@ export const handleTusUpload = (
|
||||
res.raw.end('Method not allowed')
|
||||
}
|
||||
} else {
|
||||
setCorsHeaders(res)
|
||||
tusHandler.handle(req.raw, res.raw)
|
||||
setCorsHeaders(res, isPublicEndpoint)
|
||||
void tusHandler.handle(req.raw, res.raw)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,8 @@ enum Theme {
|
||||
'Invalid DEFAULT_THEME environment variable, please select either light or dark'
|
||||
)
|
||||
|
||||
logger.info(`Portfolio ${process.env.APP_VERSION}`)
|
||||
|
||||
const server = await createServer({
|
||||
logger,
|
||||
configureApiServer: async (server) => {
|
||||
@ -52,7 +54,10 @@ enum Theme {
|
||||
datastore: new FileStore({
|
||||
directory: `./files_${isProduction ? 'prod' : 'dev'}`,
|
||||
}),
|
||||
onResponseError: (_req, res, _err) => logger.error(res),
|
||||
onResponseError(_, err) {
|
||||
logger.error(err)
|
||||
return { status_code: 500, body: 'Internal Server Error' }
|
||||
},
|
||||
})
|
||||
|
||||
server.addContentTypeParser(
|
||||
@ -60,12 +65,14 @@ enum Theme {
|
||||
(_request, _payload, done) => done(null)
|
||||
)
|
||||
|
||||
server.all('/files', (req, res) =>
|
||||
server.all('/files', (req, res) => {
|
||||
res.hijack()
|
||||
handleTusUpload(req, res, tusServer, false)
|
||||
)
|
||||
server.all('/files/*', (req, res) =>
|
||||
})
|
||||
server.all('/files/*', (req, res) => {
|
||||
res.hijack()
|
||||
handleTusUpload(req, res, tusServer, true)
|
||||
)
|
||||
})
|
||||
|
||||
await server.start()
|
||||
})()
|
||||
|
@ -5,7 +5,7 @@
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2023",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"moduleResolution": "node16",
|
||||
"skipLibCheck": false,
|
||||
"rootDirs": [
|
||||
"./src",
|
||||
|
@ -7,9 +7,9 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redwoodjs/auth-dbauth-setup": "8.4.0",
|
||||
"@redwoodjs/core": "8.4.0",
|
||||
"@redwoodjs/project-config": "8.4.0",
|
||||
"@redwoodjs/auth-dbauth-setup": "8.6.1",
|
||||
"@redwoodjs/core": "8.6.1",
|
||||
"@redwoodjs/project-config": "8.6.1",
|
||||
"prettier-plugin-tailwindcss": "0.4.1"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
@ -9,7 +9,7 @@
|
||||
title = "${FIRST_NAME} ${LAST_NAME}"
|
||||
port = 8910
|
||||
apiUrl = "/api"
|
||||
includeEnvironmentVariables = ["FIRST_NAME", "LAST_NAME", "COUNTRY", "STATE", "CITY", "DEFAULT_THEME", "API_ADDRESS_PROD", "API_ADDRESS_DEV"]
|
||||
includeEnvironmentVariables = ["FIRST_NAME", "LAST_NAME", "COUNTRY", "STATE", "CITY", "DEFAULT_THEME", "API_ADDRESS_PROD", "API_ADDRESS_DEV", "APP_VERSION"]
|
||||
[generate]
|
||||
tests = false
|
||||
stories = false
|
||||
|
@ -14,11 +14,11 @@
|
||||
"@icons-pack/react-simple-icons": "^10.0.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@redwoodjs/auth-dbauth-web": "8.4.0",
|
||||
"@redwoodjs/forms": "8.4.0",
|
||||
"@redwoodjs/router": "8.4.0",
|
||||
"@redwoodjs/web": "8.4.0",
|
||||
"@redwoodjs/web-server": "8.4.0",
|
||||
"@redwoodjs/auth-dbauth-web": "8.6.1",
|
||||
"@redwoodjs/forms": "8.6.1",
|
||||
"@redwoodjs/router": "8.6.1",
|
||||
"@redwoodjs/web": "8.6.1",
|
||||
"@redwoodjs/web-server": "8.6.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tiptap/extension-link": "^2.8.0",
|
||||
"@tiptap/extension-text-style": "^2.8.0",
|
||||
@ -41,10 +41,11 @@
|
||||
"react": "18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-html-parser": "^2.0.2"
|
||||
"react-html-parser": "^2.0.2",
|
||||
"react-pdf": "^9.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redwoodjs/vite": "8.4.0",
|
||||
"@redwoodjs/vite": "8.6.1",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/react-html-parser": "^2",
|
||||
|
@ -1,7 +1,16 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
import { mdiAlertOutline } from '@mdi/js'
|
||||
import { mdiOpenInNew } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import { Document, Page as PdfPage, pdfjs } from 'react-pdf'
|
||||
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css'
|
||||
import 'react-pdf/dist/Page/TextLayer.css'
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString()
|
||||
|
||||
interface PDFProps {
|
||||
url: string
|
||||
@ -9,33 +18,52 @@ interface PDFProps {
|
||||
}
|
||||
|
||||
const PDF = ({ url, form = false }: PDFProps) => {
|
||||
const [error, setError] = useState<boolean>(false)
|
||||
const [numPages, setNumPages] = useState<number>(0)
|
||||
function onLoadSuccess({ numPages }: { numPages: number }) {
|
||||
setNumPages(numPages)
|
||||
}
|
||||
|
||||
return error ? (
|
||||
<div role="alert" className="alert alert-warning">
|
||||
<Icon path={mdiAlertOutline} className="size-7" />
|
||||
<span>
|
||||
Could not load PDF, this is common in in-app browsers, try opening this
|
||||
page in a regular browser
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<iframe
|
||||
src={url}
|
||||
title="PDF"
|
||||
content="application/pdf"
|
||||
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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="overflow-auto flex justify-center"
|
||||
style={{
|
||||
width: 'calc(100vw - 1rem)',
|
||||
height: `calc(100vh - ${form ? '8.5rem' : '6rem'})`,
|
||||
}}
|
||||
allowFullScreen
|
||||
className="rounded-xl"
|
||||
onError={() => setError(true)}
|
||||
onLoad={() => setError(false)}
|
||||
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
loading="lazy"
|
||||
/>
|
||||
>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="fixed top-20 left-0 z-10 m-2 p-2 rounded-xl btn btn-square btn-ghost shadow-lg"
|
||||
>
|
||||
<Icon path={mdiOpenInNew} size={1} className="text-gray-600" />
|
||||
</a>
|
||||
<Document file={url} onLoadSuccess={onLoadSuccess}>
|
||||
{Array.from({ length: numPages }, (_, i) => (
|
||||
<PdfPage
|
||||
key={i}
|
||||
pageNumber={i + 1}
|
||||
width={Math.min(containerWidth, 800)}
|
||||
/>
|
||||
))}
|
||||
</Document>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -65,14 +65,30 @@ const HomePage = () => (
|
||||
</div>
|
||||
</div>
|
||||
<div className="fixed bottom-2 left-2 z-10">
|
||||
<a
|
||||
href="https://git.altaiar.dev/ahmed/portfolio"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-square"
|
||||
>
|
||||
{getLogoComponent('gitea')}
|
||||
</a>
|
||||
{process.env.APP_VERSION !== undefined ? (
|
||||
<div
|
||||
className="tooltip tooltip-right"
|
||||
data-tip={process.env.APP_VERSION}
|
||||
>
|
||||
<a
|
||||
href={`https://git.altaiar.dev/ahmed/portfolio/releases/tag/${process.env.APP_VERSION}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-square"
|
||||
>
|
||||
{getLogoComponent('gitea')}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
href={'https://git.altaiar.dev/ahmed/portfolio'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn btn-square"
|
||||
>
|
||||
{getLogoComponent('gitea')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
Reference in New Issue
Block a user