Skip to content

Commit 5c5fb62

Browse files
committed
feat: enhance Dokku plugin management and update configuration variables
- Added checks and installation commands for Dokku redirect and letsencrypt plugins in site.yml to ensure they are installed and updated to specified versions. - Updated vars.example.yml to include new variables for redirect and letsencrypt plugin versions, improving configuration clarity. - Adjusted Docker Compose configurations to use specific image versions for MinIO, Redis, and MeiliSearch, enhancing stability and predictability in deployments. - Modified cloud-init.yaml to enforce stricter sudo permissions for improved security. - Enhanced SSH CIDR validation in variables.tf to prevent the use of open access CIDR ranges.
1 parent 0044caa commit 5c5fb62

File tree

10 files changed

+491
-60
lines changed

10 files changed

+491
-60
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,6 @@ infra/hetzner/terraform.tfstate.*
9898
/src/server/function/CLAUDE.md.bak
9999
# Ansible secret overrides
100100
infra/ansible/group_vars/constructa/vars.yml
101+
.vault-pass.txt
101102
# END Ruler Generated Files
102-
.ex0
103+
.ex0

infra/ansible/group_vars/constructa/vars.example.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ constructa_registry_username: 'your-ghcr-username'
1919
constructa_registry_password: 'ghp_...'
2020

2121
constructa_redirect_plugin_repo: 'https://github.com/dokku/dokku-redirect.git'
22+
constructa_redirect_plugin_version: '0.9.1'
2223
# lets encrypt
2324
constructa_letsencrypt_plugin_repo: 'https://github.com/dokku/dokku-letsencrypt.git'
25+
constructa_letsencrypt_plugin_version: '0.22.0'
2426
constructa_letsencrypt_email: ''
2527

2628

@@ -35,8 +37,8 @@ constructa_compose_env:
3537
APP_TAG: 'latest'
3638

3739
# Compose exposes services to the host on this address. Dokku apps reach
38-
# them through host.docker.internal, so bind to 0.0.0.0 on the VPS.
39-
HOST_BIND_ADDR: '0.0.0.0'
40+
# them through host.docker.internal, so we keep them bound to 127.0.0.1 on the VPS.
41+
HOST_BIND_ADDR: '127.0.0.1'
4042
APP_HOSTNAME: 'app.example.com'
4143
ACME_EMAIL: 'admin@example.com'
4244

@@ -49,8 +51,9 @@ constructa_compose_env:
4951
MINIO_ROOT_USER: 'minioadmin'
5052
MINIO_ROOT_PASSWORD: 'minioadmin'
5153
MINIO_BUCKET: 'constructa-files'
54+
REDIS_PASSWORD: 'change-me-redis-password'
55+
REDIS_URL: 'redis://:change-me-redis-password@127.0.0.1:6379'
5256
MEILI_MASTER_KEY: 'changeme-master-key'
53-
REDIS_URL: 'redis://127.0.0.1:6379'
5457
MEILI_HOST: 'http://127.0.0.1:7700'
5558
MEILI_API_KEY: 'changeme-master-key'
5659
S3_ENDPOINT: 'http://127.0.0.1:9000'
@@ -78,7 +81,7 @@ constructa_compose_env:
7881
CHECKOUT_CANCEL_URL: 'https://app.example.com/dashboard/billing'
7982
EMAIL_PROVIDER: 'smtp'
8083
EMAIL_FROM: 'noreply@example.org'
81-
SMTP_HOST: 'smtp.sendgrid.net'
84+
SMTP_HOST: 'smtp.example.net'
8285
SMTP_PORT: '587'
8386
SMTP_SECURE: 'false'
8487
SMTP_USER: 'apikey'
@@ -106,7 +109,7 @@ constructa_swap_size: 2G
106109
constructa_dokku_config:
107110
NODE_ENV: production
108111
PORT: '5000'
109-
DOKKU_PROXY_PORT_MAP: 'http:80:5000'
112+
DOKKU_PROXY_PORT_MAP: 'http:80:5000 https:443:5000'
110113
DATABASE_URL: 'postgresql://ex0:change-me@host.docker.internal:5432/ex0'
111114
S3_ENDPOINT: 'http://host.docker.internal:9000'
112115
S3_REGION: 'us-east-1'
@@ -117,7 +120,7 @@ constructa_dokku_config:
117120
S3_PREVIEW_URL_EXPIRE_IN: '7200'
118121
MEILI_HOST: 'http://host.docker.internal:7700'
119122
MEILI_API_KEY: 'changeme-master-key'
120-
REDIS_URL: 'redis://host.docker.internal:6379'
123+
REDIS_URL: 'redis://:change-me-redis-password@host.docker.internal:6379'
121124
SEARCH_REINDEX_ON_BOOT: 'false'
122125
BULLMQ_PREFIX: 'constructa'
123126
DAILY_CREDIT_REFILL_CRON: '0 3 * * *'

infra/ansible/site.yml

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -252,17 +252,75 @@
252252
register: constructa_dokku_plugins
253253
changed_when: false
254254

255+
- name: Check if Dokku redirect plugin installed
256+
become: true
257+
command:
258+
argv:
259+
- dokku
260+
- plugin:installed
261+
- redirect
262+
register: constructa_redirect_installed
263+
changed_when: false
264+
failed_when: constructa_redirect_installed.rc not in [0, 1]
265+
when:
266+
- constructa_redirect_enabled | default(false) | bool
267+
- constructa_redirect_plugin_repo is defined
268+
- constructa_redirect_plugin_repo | length > 0
269+
255270
- name: Install Dokku redirect plugin when missing
256271
become: true
257272
command:
258273
argv:
259274
- dokku
260275
- plugin:install
261276
- "{{ constructa_redirect_plugin_repo | default('https://github.com/dokku/dokku-redirect.git') }}"
277+
- --committish
278+
- "{{ constructa_redirect_plugin_version }}"
279+
register: constructa_redirect_install
280+
when:
281+
- constructa_redirect_enabled | default(false) | bool
282+
- constructa_redirect_plugin_repo is defined
283+
- constructa_redirect_plugin_repo | length > 0
284+
- constructa_redirect_plugin_version is defined
285+
- constructa_redirect_plugin_version | length > 0
286+
- constructa_redirect_installed is defined
287+
- constructa_redirect_installed.rc != 0
288+
changed_when: true
289+
290+
- name: Pin Dokku redirect plugin to target version
291+
become: true
292+
command:
293+
argv:
294+
- dokku
295+
- plugin:update
296+
- redirect
297+
- "{{ constructa_redirect_plugin_version }}"
298+
register: constructa_redirect_update
299+
changed_when: >-
300+
constructa_redirect_update.stdout is defined and
301+
('already' not in (constructa_redirect_update.stdout | lower))
262302
when:
263303
- constructa_redirect_enabled | default(false) | bool
264-
- constructa_dokku_plugins.stdout is defined
265-
- not ('redirect' in (constructa_dokku_plugins.stdout | default('')))
304+
- constructa_redirect_plugin_repo is defined
305+
- constructa_redirect_plugin_repo | length > 0
306+
- constructa_redirect_plugin_version is defined
307+
- constructa_redirect_plugin_version | length > 0
308+
- constructa_redirect_installed is defined
309+
- constructa_redirect_installed.rc == 0
310+
311+
- name: Check if Dokku letsencrypt plugin installed
312+
become: true
313+
command:
314+
argv:
315+
- dokku
316+
- plugin:installed
317+
- letsencrypt
318+
register: constructa_letsencrypt_installed
319+
changed_when: false
320+
failed_when: constructa_letsencrypt_installed.rc not in [0, 1]
321+
when:
322+
- constructa_letsencrypt_plugin_repo is defined
323+
- constructa_letsencrypt_plugin_repo | length > 0
266324

267325
- name: Install Dokku letsencrypt plugin when missing
268326
become: true
@@ -271,11 +329,37 @@
271329
- dokku
272330
- plugin:install
273331
- "{{ constructa_letsencrypt_plugin_repo | default('https://github.com/dokku/dokku-letsencrypt.git') }}"
332+
- --committish
333+
- "{{ constructa_letsencrypt_plugin_version }}"
334+
register: constructa_letsencrypt_install
335+
when:
336+
- constructa_letsencrypt_plugin_repo is defined
337+
- constructa_letsencrypt_plugin_repo | length > 0
338+
- constructa_letsencrypt_plugin_version is defined
339+
- constructa_letsencrypt_plugin_version | length > 0
340+
- constructa_letsencrypt_installed is defined
341+
- constructa_letsencrypt_installed.rc != 0
342+
changed_when: true
343+
344+
- name: Pin Dokku letsencrypt plugin to target version
345+
become: true
346+
command:
347+
argv:
348+
- dokku
349+
- plugin:update
350+
- letsencrypt
351+
- "{{ constructa_letsencrypt_plugin_version }}"
352+
register: constructa_letsencrypt_update
353+
changed_when: >-
354+
constructa_letsencrypt_update.stdout is defined and
355+
('already' not in (constructa_letsencrypt_update.stdout | lower))
274356
when:
275357
- constructa_letsencrypt_plugin_repo is defined
276358
- constructa_letsencrypt_plugin_repo | length > 0
277-
- constructa_dokku_plugins.stdout is defined
278-
- not ('letsencrypt' in (constructa_dokku_plugins.stdout | default('')))
359+
- constructa_letsencrypt_plugin_version is defined
360+
- constructa_letsencrypt_plugin_version | length > 0
361+
- constructa_letsencrypt_installed is defined
362+
- constructa_letsencrypt_installed.rc == 0
279363

280364
- name: Ensure secondary domains exist
281365
become: true

infra/deploy/compose.yml

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: '3.9'
2-
31
x-app-image: &app_image '${APP_IMAGE:-ghcr.io/your-org/your-repo/app}:${APP_TAG:-latest}'
42

53
services:
@@ -22,7 +20,7 @@ services:
2220
retries: 20
2321

2422
minio:
25-
image: quay.io/minio/minio:latest
23+
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
2624
container_name: ex0-minio
2725
restart: unless-stopped
2826
environment:
@@ -42,7 +40,7 @@ services:
4240

4341
# one-time bucket/policy provisioning; idempotent; safe to re-run
4442
provision-minio:
45-
image: minio/mc:latest
43+
image: minio/mc:RELEASE.2025-04-16T18-13-26Z
4644
depends_on:
4745
minio:
4846
condition: service_healthy
@@ -55,10 +53,20 @@ services:
5553
restart: 'no'
5654

5755
redis:
58-
image: redis:7-alpine
56+
image: redis:8.2.1-alpine
5957
container_name: ex0-redis
6058
restart: unless-stopped
61-
command: ['redis-server', '--save', '60', '1', '--loglevel', 'warning']
59+
command:
60+
- redis-server
61+
- --bind
62+
- 127.0.0.1
63+
- --save
64+
- '60'
65+
- '1'
66+
- --loglevel
67+
- warning
68+
- --requirepass
69+
- ${REDIS_PASSWORD:?}
6270
volumes:
6371
- ex0-redis-data:/data
6472
ports:
@@ -70,7 +78,7 @@ services:
7078
retries: 20
7179

7280
meilisearch:
73-
image: getmeili/meilisearch:latest
81+
image: getmeili/meilisearch:v1.22.0
7482
container_name: ex0-meili
7583
restart: unless-stopped
7684
environment:
@@ -126,7 +134,7 @@ services:
126134
S3_ENABLE_PATH_STYLE: ${S3_ENABLE_PATH_STYLE:-1}
127135
S3_PREVIEW_URL_EXPIRE_IN: ${S3_PREVIEW_URL_EXPIRE_IN:-7200}
128136
# Queue + schedules
129-
REDIS_URL: ${REDIS_URL:-redis://127.0.0.1:6379}
137+
REDIS_URL: redis://:${REDIS_PASSWORD:?}@127.0.0.1:6379
130138
BULLMQ_PREFIX: ${BULLMQ_PREFIX:-constructa}
131139
DAILY_CREDIT_REFILL_CRON: ${DAILY_CREDIT_REFILL_CRON:-0 3 * * *}
132140
JOB_DAILY_CREDIT_REFILL_URL: ${JOB_DAILY_CREDIT_REFILL_URL:-http://app:3000/api/jobs/daily-credit-refill}

infra/hetzner/cloud-init.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ users:
33
- name: ${deploy_username}
44
groups: [sudo]
55
shell: /bin/bash
6-
sudo: ALL=(ALL) NOPASSWD:ALL
6+
sudo: ALL=(ALL) ALL
77
ssh_authorized_keys:
88
- ${deploy_ssh_pubkey}
99

@@ -29,7 +29,6 @@ runcmd:
2929
- bash -c 'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $${VERSION_CODENAME}) stable" > /etc/apt/sources.list.d/docker.list'
3030
- apt-get update
3131
- apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
32-
- usermod -aG docker ${deploy_username}
3332
- systemctl enable --now docker
3433

3534
# Wait for Docker to be ready

infra/hetzner/variables.tf

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,14 @@ variable "ssh_public_key_path" {
3131
variable "allowed_ssh_cidr" {
3232
description = "CIDR allowed to SSH (e.g., your IP /32)"
3333
type = string
34-
default = "0.0.0.0/0"
34+
35+
validation {
36+
condition = !contains(["0.0.0.0/0", "::/0"], var.allowed_ssh_cidr) && anytrue([
37+
can(cidrnetmask(var.allowed_ssh_cidr)),
38+
can(cidrhost(var.allowed_ssh_cidr, 0))
39+
])
40+
error_message = "Provide a specific CIDR (for example 198.51.100.23/32) instead of 0.0.0.0/0 or ::/0."
41+
}
3542
}
3643

3744
variable "enable_http3" {
@@ -44,4 +51,4 @@ variable "deploy_username" {
4451
description = "Non-root user for SSH deploy"
4552
type = string
4653
default = "deploy"
47-
}
54+
}

src/components/auth/auth-styles.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export const authLocalizationOverrides: Partial<AuthLocalization> = {
6767
TERMS_OF_SERVICE: 'Terms of Service',
6868
PRIVACY_POLICY: 'Privacy Policy',
6969
REQUEST_FAILED: 'An error occurred. Please try again.',
70+
OR_CONTINUE_WITH: 'Or continue with',
71+
SIGN_IN_WITH: 'Continue with',
7072
};
7173

7274
// Container styling for auth pages
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { ComponentType, SVGProps } from 'react';
2+
import type { SocialProvider } from 'better-auth/social-providers';
3+
import RiGithubFill from '~icons/ri/github-fill';
4+
import RiGoogleFill from '~icons/ri/google-fill';
5+
6+
export type SupportedSocialProvider = Extract<SocialProvider, 'github' | 'google'>;
7+
8+
export type SocialProviderDisplay = {
9+
readonly id: SupportedSocialProvider;
10+
readonly label: string;
11+
readonly cta: string;
12+
readonly icon: ComponentType<SVGProps<SVGSVGElement>>;
13+
readonly buttonClassName: string;
14+
readonly iconClassName?: string;
15+
};
16+
17+
const baseButtonClass =
18+
'h-11 w-full justify-center gap-2 rounded-lg border transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2';
19+
20+
export const SOCIAL_PROVIDER_DISPLAYS: Record<SupportedSocialProvider, SocialProviderDisplay> = {
21+
github: {
22+
id: 'github',
23+
label: 'Continue with GitHub',
24+
cta: 'Sign in with GitHub',
25+
icon: RiGithubFill,
26+
buttonClassName: `${baseButtonClass} border-transparent bg-[#1F2328] text-white shadow-sm hover:bg-[#161B22] focus-visible:ring-[#8C949E] dark:bg-[#0D1117] dark:hover:bg-[#161B22]`,
27+
iconClassName: 'size-5',
28+
},
29+
google: {
30+
id: 'google',
31+
label: 'Continue with Google',
32+
cta: 'Sign in with Google',
33+
icon: RiGoogleFill,
34+
buttonClassName: `${baseButtonClass} border-input bg-white text-neutral-900 shadow-sm hover:bg-neutral-100 focus-visible:ring-[#4285F4] dark:text-neutral-900`,
35+
iconClassName: 'size-5',
36+
},
37+
};
38+
39+
export function resolveSocialProviderDisplays(
40+
providers: ReadonlyArray<SocialProvider> | undefined
41+
): SocialProviderDisplay[] {
42+
if (!providers?.length) {
43+
return [];
44+
}
45+
46+
return providers
47+
.map((provider) => SOCIAL_PROVIDER_DISPLAYS[provider as SupportedSocialProvider])
48+
.filter((value): value is SocialProviderDisplay => Boolean(value));
49+
}

src/routes/__root.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ function RootComponent() {
9696
function RootDocument({ children }: { children: React.ReactNode }) {
9797
const initial = Route.useLoaderData() as Theme;
9898
const router = useRouter();
99+
const hasGithub = !!import.meta.env.VITE_GITHUB_CLIENT_ID;
100+
const hasGoogle = !!import.meta.env.VITE_GOOGLE_CLIENT_ID;
101+
const socialProviders = [
102+
...(hasGithub ? ['github'] : []),
103+
...(hasGoogle ? ['google'] : []),
104+
];
99105

100106
return (
101107
<html lang="en" className={initial === 'system' ? '' : initial}>
@@ -110,9 +116,14 @@ function RootDocument({ children }: { children: React.ReactNode }) {
110116
<AuthUIProviderTanstack
111117
authClient={authClient}
112118
redirectTo="/dashboard"
113-
navigate={(href) => router.navigate({ href })}
114-
replace={(href) => router.navigate({ href, replace: true })}
119+
navigate={(to) => router.navigate({ to })}
120+
replace={(to) => router.navigate({ to, replace: true })}
115121
Link={({ href, ...props }) => <Link to={href} {...props} />}
122+
social={
123+
socialProviders.length > 0
124+
? { providers: socialProviders }
125+
: undefined
126+
}
116127
>
117128
<div className="flex min-h-svh flex-col">{children}</div>
118129
<Toaster />

0 commit comments

Comments
 (0)