Compare commits
17 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d215052e0 | ||
|
|
a90453eefc | ||
|
|
83ea4afbdf | ||
|
|
93b7bfffea | ||
|
|
ffc43e9071 | ||
|
|
03ee678059 | ||
|
|
9266599b6a | ||
|
|
39903bc9f1 | ||
|
|
8cb7b672d6 | ||
|
|
a94bc545aa | ||
|
|
94e700d2e7 | ||
|
|
c3cdc0073c | ||
|
|
3debc5609a | ||
|
|
1fede2034e | ||
|
|
ef79078b06 | ||
|
|
76fb9b22ff | ||
|
|
261b8cbed7 |
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,4 +1,6 @@
|
|||
/graphics
|
||||
.DS_Store
|
||||
**/*.secret
|
||||
|
||||
**/*.pw
|
||||
**/*.secret
|
||||
mounts/
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@ RUN --mount=type=cache,target=/usr/local/cargo,from=rust:latest,source=/usr/loca
|
|||
cargo build --release --features $adaptor && mv ./target/release/crabfit-api ./api
|
||||
|
||||
# Runtime image
|
||||
FROM debian:bullseye-slim
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# install libssl3
|
||||
RUN apt-get update &&\
|
||||
apt-get install -yq libssl3 &&\
|
||||
apt-get clean &&\
|
||||
rm -rf /var/cache/apt/lists/*
|
||||
|
||||
# Run as "app" user
|
||||
RUN useradd -ms /bin/bash app
|
||||
|
|
|
|||
|
|
@ -188,11 +188,14 @@ fn get_connection_string() -> String {
|
|||
if let Some(password_file_location) = env::var_os("DATABASE_PASSWORD_FILE") {
|
||||
// The password can be left out of the URL, we add it from the specified
|
||||
// file (presumably under /run/secrets/)
|
||||
let password = fs::read(&password_file_location).unwrap_or_else(|err| {
|
||||
let password = fs::read(&password_file_location)
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("could not read database password from {password_file_location:?}\n\t{err:?}")
|
||||
});
|
||||
let password = String::from(String::from_utf8_lossy(password.as_slice()));
|
||||
let password = password.trim_end();
|
||||
let mut url = Url::parse(&connection_string).expect("invalid connection string");
|
||||
url.set_password(Some(String::from_utf8_lossy(password.as_slice()).as_ref()))
|
||||
url.set_password(Some(password))
|
||||
.unwrap_or_else(|_| panic!("invalid database URL: {connection_string:?}"));
|
||||
url.to_string()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ pub async fn cleanup<A: Adaptor>(
|
|||
println!("Error reading CRON_KEY_FILE at {path:?}");
|
||||
return Err(ApiError::NotAuthorized);
|
||||
};
|
||||
String::from_utf8_lossy(key.as_slice()).into()
|
||||
String::from_utf8_lossy(key.as_slice()).to_owned().trim_end().to_string()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
|
|
|||
60
docker-compose.yml
Normal file
60
docker-compose.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
services:
|
||||
crabfit-api:
|
||||
build:
|
||||
context: ./api
|
||||
# args:
|
||||
# adaptor: sql-adaptor (default) | memory-adaptor | datastore-adaptor
|
||||
# # datastore is for Google Datastore
|
||||
secrets:
|
||||
- crabfit-database-password
|
||||
- crabfit-cron-key
|
||||
environment:
|
||||
DATABASE_PASSWORD_FILE: /run/secrets/crabfit-database-password
|
||||
DATABASE_URL: postgresql://crabfit@crabfit-database:5432/crabfit
|
||||
FRONTEND_URL: https://availability.techwork.zone
|
||||
CRON_KEY_FILE: /run/secrets/crabfit-cron-key
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.crabfit-api.rule: Host(`api.a10y.techwork.zone`)
|
||||
traefik.http.routers.crabfit-api.tls: true
|
||||
traefik.http.routers.crabfit-api.tls.certresolver: letsencrypt_standalone
|
||||
networks:
|
||||
- crabfit
|
||||
- public
|
||||
|
||||
crabfit-database:
|
||||
image: postgres:17
|
||||
secrets: [ 'crabfit-database-password' ]
|
||||
environment:
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/crabfit-database-password
|
||||
POSTGRES_USER: crabfit
|
||||
POSTGRES_DB: crabfit
|
||||
volumes:
|
||||
- ./mounts/database:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready"]
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
networks: [ crabfit ]
|
||||
|
||||
crabfit-frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.crabfit-frontend.rule: Host(`a10y.techwork.zone`) || Host(`availability.techwork.zone`)
|
||||
traefik.http.routers.crabfit-frontend.tls: true
|
||||
traefik.http.routers.crabfit-frontend.tls.certresolver: letsencrypt_standalone
|
||||
networks: [ public ]
|
||||
|
||||
|
||||
networks:
|
||||
crabfit:
|
||||
internal: true
|
||||
|
||||
secrets:
|
||||
crabfit-database-password:
|
||||
file: ./postgres.pw
|
||||
crabfit-cron-key:
|
||||
file: ./cron.secret
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
NEXT_PUBLIC_API_URL="http://127.0.0.1:3000"
|
||||
NEXT_PUBLIC_API_URL="https://api.a10y.techwork.zone"
|
||||
NEXT_PUBLIC_HOSTNAME=availability.techwork.zone
|
||||
|
||||
# Google auth for calendar syncing, feature will be disabled if these aren't set
|
||||
# NEXT_PUBLIC_GOOGLE_CLIENT_ID=""
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ ENV NODE_ENV=production
|
|||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1000 nodejs
|
||||
RUN adduser --system --uid 1000 nextjs
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
|
|
|
|||
4
frontend/next.config.js
Normal file
4
frontend/next.config.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
output: 'standalone'
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
"@giraugh/tools": "^1.6.0",
|
||||
"@js-temporal/polyfill": "^0.4.4",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.5",
|
||||
"@vercel/analytics": "^1.0.1",
|
||||
"accept-language": "^3.0.18",
|
||||
"chroma.ts": "^1.0.10",
|
||||
"hue-map": "^1.0.0",
|
||||
|
|
@ -47,5 +46,10 @@
|
|||
"sass": "^1.63.4",
|
||||
"typescript": "^5.1.3",
|
||||
"typescript-plugin-css-modules": "^5.0.1"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { makeClass, relativeTimeFormat } from '/src/utils'
|
|||
|
||||
import EventAvailabilities from './EventAvailabilities'
|
||||
import styles from './page.module.scss'
|
||||
import appLink from '/src/utils/appLink'
|
||||
|
||||
interface PageProps {
|
||||
params: { id: string }
|
||||
|
|
@ -49,10 +50,10 @@ const Page = async ({ params }: PageProps) => {
|
|||
>{t('common:created', { date: relativeTimeFormat(Temporal.Instant.fromEpochSeconds(event.created_at), i18n.language) })}</span>
|
||||
|
||||
<Copyable className={styles.info}>
|
||||
{`https://crab.fit/${event.id}`}
|
||||
{appLink(event.id)}
|
||||
</Copyable>
|
||||
<p className={makeClass(styles.info, styles.noPrint)}>
|
||||
<Trans i18nKey="event:nav.shareinfo" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${event.id}`)}`}>_</a>_</Trans>
|
||||
<Trans i18nKey="event:nav.shareinfo" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} ${appLink(event.id)}`)}`}>_</a>_</Trans>
|
||||
</p>
|
||||
</Content>
|
||||
</Suspense>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Metadata } from 'next'
|
||||
import { Karla } from 'next/font/google'
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
|
||||
import Egg from '/src/components/Egg/Egg'
|
||||
import Settings from '/src/components/Settings/Settings'
|
||||
|
|
@ -9,11 +8,12 @@ import { fallbackLng } from '/src/i18n/options'
|
|||
import { useTranslation } from '/src/i18n/server'
|
||||
|
||||
import './global.css'
|
||||
import appLink from '../utils/appLink'
|
||||
|
||||
const karla = Karla({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://crab.fit'),
|
||||
metadataBase: new URL(appLink('')),
|
||||
title: {
|
||||
absolute: 'Crab Fit',
|
||||
template: '%s - Crab Fit',
|
||||
|
|
@ -44,7 +44,6 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
|||
|
||||
{children}
|
||||
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ const Page = async () => {
|
|||
|
||||
<Video />
|
||||
|
||||
<DownloadButtons />
|
||||
{/* <DownloadButtons /> */}
|
||||
|
||||
<P><Trans i18nKey="about.content.p3" t={t} i18n={i18n}>_<a href="https://bengrant.dev" target="_blank" rel="noreferrer noopener author">_</a>_</Trans></P>
|
||||
<P><Trans i18nKey="about.content.p4" t={t} i18n={i18n}>_<a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer noopener">_</a>_<Link href="/privacy" rel="license">_</Link>_</Trans></P>
|
||||
<P><Trans i18nKey="about.content.p3" t={t} i18n={i18n}>_<a href="https://bengrant.dev" target="_blank" rel="noreferrer noopener author">_</a><a href="https://techwork.zone" target="_blank" rel="noreferrer noopener">_</a>_</Trans></P>
|
||||
<P><Trans i18nKey="about.content.p4" t={t} i18n={i18n}>_<a href="https://git.techwork.zone/scott/crabfit" target="_blank" rel="noreferrer noopener">_</a>_<Link href="/privacy" rel="license">_</Link>_</Trans></P>
|
||||
<P>{t('about.content.p6')}</P>
|
||||
<P>{t('about.content.p5')}</P>
|
||||
</Content>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { EventResponse } from '/src/config/api'
|
|||
import { useTranslation } from '/src/i18n/client'
|
||||
|
||||
import styles from './EventInfo.module.scss'
|
||||
import appLink from '/src/utils/appLink'
|
||||
|
||||
interface EventInfoProps {
|
||||
event: EventResponse
|
||||
|
|
@ -16,10 +17,10 @@ const EventInfo = ({ event }: EventInfoProps) => {
|
|||
return <div className={styles.wrapper}>
|
||||
<h2>{event.name}</h2>
|
||||
<Copyable className={styles.info}>
|
||||
{`https://crab.fit/${event.id}`}
|
||||
{appLink(event.id)}
|
||||
</Copyable>
|
||||
<p className={styles.info}>
|
||||
<Trans i18nKey="event:nav.shareinfo_alt" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t('nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('nav.email_body')} https://crab.fit/${event.id}`)}`} target="_blank">_</a>_</Trans>
|
||||
<Trans i18nKey="event:nav.shareinfo_alt" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t('nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('nav.email_body')} ${appLink(event.id)}`)}`} target="_blank">_</a>_</Trans>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import logo from '/src/res/logo.svg'
|
|||
import { makeClass } from '/src/utils'
|
||||
|
||||
import styles from './Header.module.scss'
|
||||
import appLink from '/src/utils/appLink'
|
||||
|
||||
const samuraiBob = localFont({
|
||||
src: './samuraibob.woff2',
|
||||
|
|
@ -29,11 +30,11 @@ const Header = async ({ isFull, isSmall }: HeaderProps) => {
|
|||
{isFull ? <>
|
||||
{!isSmall && <img className={styles.bigLogo} src={logo.src} height={512} width={512} alt="" />}
|
||||
<span className={makeClass(styles.subtitle, samuraiBob.className, !/^[A-Za-z ]+$/.test(t('home:create')) && styles.hasAltChars)}>{t('home:create')}</span>
|
||||
<h1 className={makeClass(styles.bigTitle, molot.className)}>CRAB FIT</h1>
|
||||
<h1 className={makeClass(styles.bigTitle, molot.className)}>MEETING</h1>
|
||||
</> : <Link href="/" className={styles.link}>
|
||||
<div className={styles.top}>
|
||||
<img className={styles.logo} src={logo.src} height={512} width={512} alt="" />
|
||||
<span className={makeClass(styles.title, molot.className)}>CRAB FIT</span>
|
||||
<span className={makeClass(styles.title, molot.className)}>MEETING</span>
|
||||
</div>
|
||||
<span className={styles.tagline}>{t('common:tagline')}</span>
|
||||
</Link>}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"cta": "Create your own Crab Fit!",
|
||||
"created": "Created {{date}}",
|
||||
"donate": {
|
||||
"info": "Thank you for using Crab Fit. If you like it, consider donating.",
|
||||
"info": "Thank you for using Crab Fit. If you like it, consider donating to the upstream author.",
|
||||
"button": "Donate",
|
||||
"title": "Every amount counts :)"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@
|
|||
"events": "Events created",
|
||||
"availabilities": "Availabilities entered",
|
||||
"content": {
|
||||
"p1": "Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.<1/><2>Learn more about how to Crab Fit</2>.",
|
||||
"p3": "Created by <1>Ben Grant</1>, Crab Fit is the modern-day solution to your group event planning debates.",
|
||||
"p1": "This service is a soft fork of Crab Fit. Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.<1/><2>Learn more about how to Crab Fit</2>.",
|
||||
"p3": "Created by <1>Ben Grant</1> and hosted by the <2>Tech Workers' Syndicate</2>, Crab Fit is the modern-day solution to your group event planning debates.",
|
||||
"p4": "The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <1>repository</1>. By using Crab Fit you agree to the <3>privacy policy</3>.",
|
||||
"p6": "To protect your privacy, events are deleted after 3 months of inactivity, and all passwords are securely hashed.",
|
||||
"p5": "Consider donating below if it helped you out so Crab Fit can stay free for everyone. 🦀"
|
||||
|
|
|
|||
7
frontend/src/utils/appLink.tsx
Normal file
7
frontend/src/utils/appLink.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function appLink(path: string) {
|
||||
const proto = process?.env?.NODE_ENV === 'production' ? 'https' : 'http'
|
||||
const host =
|
||||
process?.env?.NEXT_PUBLIC_HOSTNAME ??
|
||||
(process?.env?.NODE_ENV === 'production' ? 'crab.fit' : 'localhost:3000')
|
||||
return `${proto}://${host}/${path}`
|
||||
}
|
||||
Loading…
Reference in a new issue