6 Commits

Author SHA1 Message Date
f8987b08da Add persistent flag of origin
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 1m12s
2024-10-17 21:36:50 -04:00
cbf75acbeb Fix empty images section if no images + Remove postgres port 2024-10-17 20:46:34 -04:00
7973663b2a Allow separate from and to emails
All checks were successful
Publish Docker Image / Publish Docker Image (push) Successful in 10s
2024-10-16 21:45:55 -04:00
6540329f36 Use SMTP credentials instead of Gmail auth for Email 2024-10-16 21:36:57 -04:00
bac5b5fe48 Revert prefers-color-scheme 2024-10-16 21:21:58 -04:00
f3f75d3e57 Fix file uploads clearing on upgrade 2024-10-15 22:51:41 -04:00
16 changed files with 134 additions and 58 deletions

View File

@ -18,8 +18,15 @@ PRISMA_HIDE_UPDATE_MESSAGE=true
FIRST_NAME=firstname
LAST_NAME=lastname
GMAIL=example@gmail.com
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
COUNTRY=US
SMTP_HOST=smtp.example.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=noreply@example.com
EMAIL_FROM=noreply@example.com
EMAIL_TO=email@example.com
SMTP_PASSWORD=password
DOMAIN=example.com
API_DOMAIN=api.example.com

View File

@ -6,8 +6,15 @@
FIRST_NAME=firstname
LAST_NAME=lastname
GMAIL=example@gmail.com
GMAIL_SMTP_PASSWORD=chan geme xyza bcde
COUNTRY=US
SMTP_HOST=smtp.example.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=noreply@example.com
EMAIL_FROM=noreply@example.com
EMAIL_TO=email@example.com
SMTP_PASSWORD=password
DOMAIN=example.com
API_DOMAIN=api.example.com

View File

@ -43,8 +43,13 @@ ARG ADDRESS_DEV
ARG DOMAIN
ARG API_DOMAIN
ARG MAX_HTTP_CONNECTIONS_PER_MINUTE
ARG GMAIL
ARG GMAIL_SMTP_PASSWORD
ARG SMTP_HOST
ARG SMTP_PORT
ARG SMTP_SECURE
ARG SMTP_USER
ARG SMTP_PASSWORD
ARG EMAIL_FROM
ARG EMAIL_TO
ARG FIRST_NAME
ARG LAST_NAME

View File

@ -6,10 +6,8 @@ Create two A records, one for the web side of the website and one for the api si
- It doesn't matter what reverse proxy you use (Nginx, Apache, Traefik, Caddy, etc)
1. Point the web domain to the web port (default: 8910)
2. Point the api domain to the api port (default: 8911)
### Gmail App Password
1. Go to your Google [account dashboard](https://myaccount.google.com)
2. Go to Security > 2-Step Verification > App Passwords > Create a new app password
3. Copy the 16 character password
### SMTP
You will need credentials to authorize sending Email, instructions vary depending on provider (Gmail, Hotmail, etc).
### [Docker Compose](./docker-compose.yml)
```yaml
version: '3.8'
@ -23,21 +21,29 @@ services:
- API_PROXY_TARGET=http://localhost:8911
- MAX_HTTP_CONNECTIONS_PER_MINUTE=60
- SESSION_SECRET=super_secret_session_key_change_me_in_production_please
- DATABASE_URL=postgresql://redwood:redwood@db:5432/portfolio
- DATABASE_URL=postgresql://redwood:changeme@db/portfolio
- FIRST_NAME=first name # Your first name
- LAST_NAME=lastname # Your last name
- GMAIL=example@gmail.com # The Gmail address associated with the app password created earlier
- GMAIL_SMTP_PASSWORD=aaaa bbbb cccc dddd # The app password created earlier
- COUNTRY=US # ISO-3166-1 alpha-2 country code, https://en.wikipedia.org/wiki/ISO_3166-1#Codes
- SMTP_HOST=smtp.example.com
- SMTP_PORT=465
- SMTP_SECURE=true
- SMTP_USER=noreply@example.com
- EMAIL_FROM=noreply@example.com
- EMAIL_TO=email@example.com
- SMTP_PASSWORD=password
- DOMAIN=portfolio.example.com
- API_DOMAIN=api.portfolio.example.com
# Careful, addresses below must not end with a '/'
- ADDRESS_PROD=https://portfolio.example.com # https://DOMAIN
- API_ADDRESS_PROD=https://api.portfolio.example.com # https://API_DOMAIN
ports:
- '8910:8910' # Web
- '8911:8911' # API
- 8910:8910 # Web
- 8911:8911 # API
depends_on:
- db
volumes:
- files:/home/node/app/api/files_prod
command: >
/bin/sh -c "
yarn rw build &&
@ -50,23 +56,27 @@ services:
container_name: portfolio-db
image: postgres:16-bookworm
environment:
POSTGRES_USER: redwood
POSTGRES_PASSWORD: redwood
POSTGRES_DB: portfolio
ports:
- '5432:5432'
- POSTGRES_USER=redwood
- POSTGRES_PASSWORD=changeme
- POSTGRES_DB=portfolio
volumes:
- postgres:/var/lib/postgresql/data
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`
- If you correctly set up the Gmail app password, you should receive an email from yourself
- It contains the link needed to change your password
- If you correctly configured [SMTP](#smtp), you should receive an Email from [`EMAIL_FROM`](#docker-compose) to [`EMAIL_TO`](#docker-compose)
- The Email contains the link needed to change your password
### Default Credentials
**Username:** `admin`
**Password:** [`GMAIL_SMTP_PASSWORD`](#gmail-app-password)
**Password:** [`SMTP_PASSWORD`](#docker-compose)

View File

@ -11,6 +11,7 @@
"@redwoodjs/graphql-server": "8.4.0",
"@tus/file-store": "^1.4.0",
"@tus/server": "^1.7.0",
"country-flag-icons": "^1.5.13",
"graphql-scalars": "^1.23.0",
"nodemailer": "^6.9.14"
},

View File

@ -8,16 +8,18 @@ interface Options {
}
const transporter = nodemailer.createTransport({
service: 'gmail',
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.GMAIL,
pass: process.env.GMAIL_SMTP_PASSWORD,
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
})
export const sendEmail = async ({ to, subject, text, html }: Options) =>
await transporter.sendMail({
from: `"${process.env.FIRST_NAME} ${process.env.LAST_NAME} (noreply)" <${process.env.GMAIL}>`,
from: `${process.env.FIRST_NAME} ${process.env.LAST_NAME} <${process.env.EMAIL_FROM}>`,
to: Array.isArray(to) ? to : [to],
subject,
text,

View File

@ -10,6 +10,13 @@ import { createServer } from '@redwoodjs/api-server'
import { logger } from 'src/lib/logger'
import { handleTusUpload } from 'src/lib/tus'
;(async () => {
const { hasFlag } = await import('country-flag-icons')
if (!hasFlag(process.env.COUNTRY))
throw new Error(
'Invalid COUNTRY environment variable, please select a valid ISO-3166-1 alpha-2 country code\n See https://en.wikipedia.org/wiki/ISO_3166-1#Codes'
)
const server = await createServer({
logger,
configureApiServer: async (server) => {

View File

@ -9,21 +9,29 @@ services:
- API_PROXY_TARGET=http://localhost:8911
- MAX_HTTP_CONNECTIONS_PER_MINUTE=60
- SESSION_SECRET=super_secret_session_key_change_me_in_production_please
- DATABASE_URL=postgresql://redwood:redwood@db:5432/portfolio
- DATABASE_URL=postgresql://redwood:changeme@db/portfolio
- FIRST_NAME=first name # Your first name
- LAST_NAME=lastname # Your last name
- GMAIL=example@gmail.com # The Gmail address associated with the app password created earlier
- GMAIL_SMTP_PASSWORD=aaaa bbbb cccc dddd # The app password created earlier
- COUNTRY=US # ISO-3166-1 alpha-2 country code, https://en.wikipedia.org/wiki/ISO_3166-1#Codes
- SMTP_HOST=smtp.example.com
- SMTP_PORT=465
- SMTP_SECURE=true
- SMTP_USER=noreply@example.com
- EMAIL_FROM=noreply@example.com
- EMAIL_TO=email@example.com
- SMTP_PASSWORD=password
- DOMAIN=portfolio.example.com
- API_DOMAIN=api.portfolio.example.com
# Careful, addresses below must not end with a '/'
- ADDRESS_PROD=https://portfolio.example.com # https://DOMAIN
- API_ADDRESS_PROD=https://api.portfolio.example.com # https://API_DOMAIN
ports:
- '8910:8910' # Web
- '8911:8911' # API
- 8910:8910 # Web
- 8911:8911 # API
depends_on:
- db
volumes:
- files:/home/node/app/api/files_prod
command: >
/bin/sh -c "
yarn rw build &&
@ -36,13 +44,12 @@ services:
container_name: portfolio-db
image: postgres:16-bookworm
environment:
POSTGRES_USER: redwood
POSTGRES_PASSWORD: redwood
POSTGRES_DB: portfolio
ports:
- '5432:5432'
- POSTGRES_USER=redwood
- POSTGRES_PASSWORD=changeme
- POSTGRES_DB=portfolio
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
files: # For persistent file storage across upgrades

View File

@ -9,7 +9,7 @@
title = "${FIRST_NAME} ${LAST_NAME}"
port = 8910
apiUrl = "/api"
includeEnvironmentVariables = ["FIRST_NAME", "LAST_NAME", "API_ADDRESS_PROD", "API_ADDRESS_DEV"]
includeEnvironmentVariables = ["FIRST_NAME", "LAST_NAME", "COUNTRY", "API_ADDRESS_PROD", "API_ADDRESS_DEV"]
[generate]
tests = false
stories = false

View File

@ -9,15 +9,15 @@ export default async () => {
try {
const admin = {
username: 'admin',
email: process.env.GMAIL,
password: process.env.GMAIL_SMTP_PASSWORD,
email: process.env.EMAIL_TO,
password: process.env.SMTP_PASSWORD,
}
const [hashedPassword, salt] = hashPassword(admin.password)
const existingAdmin = await db.user.findFirst({
where: {
email: admin.email,
username: admin.username,
},
})
@ -30,6 +30,14 @@ export default async () => {
salt,
},
})
else
await db.user.update({
where: { id: existingAdmin.id },
data: {
username: admin.username,
email: admin.email,
},
})
const titles = await db.titles.findFirst()

View File

@ -35,6 +35,7 @@
"@uppy/react": "^4.0.1",
"@uppy/tus": "^4.0.0",
"@uppy/webcam": "^4.0.1",
"country-flag-icons": "^1.5.13",
"date-fns": "^4.1.0",
"humanize-string": "2.1.0",
"react": "18.3.1",

View File

@ -69,20 +69,23 @@ const Project = ({ project }: Props) => {
</div>
</>
)}
<h2 className="sm:hidden font-bold text-3xl text-center">Images</h2>
{project.images.length > 0 && (
<h2 className="sm:hidden font-bold text-3xl text-center">Images</h2>
)}
</div>
<div className="flex flex-wrap gap-4 items-center pt-8 justify-center">
{project.images.map((image, i) => (
<a
href={image}
target="_blank"
rel="noreferrer"
key={i}
className="rounded-xl"
>
<img src={image} alt="" className="rounded-xl" />
</a>
))}
<div className="flex flex-wrap gap-4 pt-8 justify-center h-fit">
{project.images.length > 0 &&
project.images.map((image, i) => (
<a
href={image}
target="_blank"
rel="noreferrer"
key={i}
className="rounded-xl"
>
<img src={image} alt="" className="rounded-xl" />
</a>
))}
</div>
</div>
)

View File

@ -8,10 +8,7 @@ const DARK_THEME = 'dark'
const ThemeToggle = () => {
const [theme, setTheme] = useState(
(localStorage.getItem('theme') ??
window.matchMedia('(prefers-color-scheme: dark)').matches)
? DARK_THEME
: LIGHT_THEME
localStorage.getItem('theme') ?? LIGHT_THEME
)
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -1,3 +1,4 @@
import { hasFlag } from 'country-flag-icons'
import { hydrateRoot, createRoot } from 'react-dom/client'
import App from 'src/App'
@ -15,6 +16,11 @@ if (!redwoodAppElement)
"exists in your 'web/src/index.html' file."
)
if (!hasFlag(process.env.COUNTRY))
throw new Error(
'Invalid COUNTRY environment variable, please select a valid ISO-3166-1 alpha-2 country code\n See https://en.wikipedia.org/wiki/ISO_3166-1#Codes'
)
if (redwoodAppElement.children?.length > 0)
hydrateRoot(redwoodAppElement, <App />)
else {

View File

@ -1,5 +1,6 @@
import { mdiCompass, mdiContacts } from '@mdi/js'
import Icon from '@mdi/react'
import getUnicodeFlagIcon from 'country-flag-icons/unicode'
import { Link, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
@ -44,6 +45,11 @@ const HomePage = () => (
{getLogoComponent('gitea')}
</a>
</div>
<div className="fixed bottom-2 right-2 z-10">
<p className="btn btn-square text-xl">
{getUnicodeFlagIcon(process.env.COUNTRY)}
</p>
</div>
</>
)

View File

@ -7336,6 +7336,7 @@ __metadata:
"@tus/file-store": "npm:^1.4.0"
"@tus/server": "npm:^1.7.0"
"@types/nodemailer": "npm:^6.4.15"
country-flag-icons: "npm:^1.5.13"
graphql-scalars: "npm:^1.23.0"
nodemailer: "npm:^6.9.14"
languageName: unknown
@ -9061,6 +9062,13 @@ __metadata:
languageName: node
linkType: hard
"country-flag-icons@npm:^1.5.13":
version: 1.5.13
resolution: "country-flag-icons@npm:1.5.13"
checksum: 10c0/beee2fe225469507d6c8df90376e031f08a5f103f65cd68e1db0679e82d4ffb2fbb27a3bb19defd112745b5c19d1972df615df21813c8c2074062dd5eb08eabb
languageName: node
linkType: hard
"crc-32@npm:^1.2.0":
version: 1.2.2
resolution: "crc-32@npm:1.2.2"
@ -19234,6 +19242,7 @@ __metadata:
"@uppy/tus": "npm:^4.0.0"
"@uppy/webcam": "npm:^4.0.1"
autoprefixer: "npm:^10.4.20"
country-flag-icons: "npm:^1.5.13"
daisyui: "npm:^4.12.10"
date-fns: "npm:^4.1.0"
humanize-string: "npm:2.1.0"