fix ssh key parsing
This commit is contained in:
+17
-2
@@ -190,6 +190,12 @@ deploy_to_vps:
|
|||||||
- eval $(ssh-agent -s)
|
- eval $(ssh-agent -s)
|
||||||
- |
|
- |
|
||||||
key_file="$(mktemp)"
|
key_file="$(mktemp)"
|
||||||
|
decoded_key_file="$(mktemp)"
|
||||||
|
|
||||||
|
cleanup_key_files() {
|
||||||
|
rm -f "$key_file" "$decoded_key_file"
|
||||||
|
}
|
||||||
|
trap cleanup_key_files EXIT
|
||||||
|
|
||||||
if [ -f "${SSH_PRIVATE_KEY:-}" ]; then
|
if [ -f "${SSH_PRIVATE_KEY:-}" ]; then
|
||||||
tr -d '\r' < "$SSH_PRIVATE_KEY" > "$key_file"
|
tr -d '\r' < "$SSH_PRIVATE_KEY" > "$key_file"
|
||||||
@@ -198,13 +204,22 @@ deploy_to_vps:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
chmod 600 "$key_file"
|
chmod 600 "$key_file"
|
||||||
|
|
||||||
|
if ! ssh-keygen -y -f "$key_file" >/dev/null 2>&1; then
|
||||||
|
if [ ! -f "${SSH_PRIVATE_KEY:-}" ] && printf '%s' "$SSH_PRIVATE_KEY" | tr -d '\r\n\t ' | base64 -d > "$decoded_key_file" 2>/dev/null; then
|
||||||
|
tr -d '\r' < "$decoded_key_file" > "$key_file"
|
||||||
|
chmod 600 "$key_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
ssh-keygen -y -f "$key_file" >/dev/null 2>&1 || {
|
ssh-keygen -y -f "$key_file" >/dev/null 2>&1 || {
|
||||||
echo "SSH_PRIVATE_KEY is not a valid unencrypted private key that OpenSSH can read." >&2
|
echo "SSH_PRIVATE_KEY must be an unencrypted OpenSSH private key. GitLab file variables, raw multiline keys, and base64-encoded keys are supported." >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
ssh-add "$key_file"
|
ssh-add "$key_file"
|
||||||
rm -f "$key_file"
|
cleanup_key_files
|
||||||
|
trap - EXIT
|
||||||
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
# Scan and trust the VPS host key (avoids manual known_hosts management)
|
# Scan and trust the VPS host key (avoids manual known_hosts management)
|
||||||
- ssh-keyscan -H $VPS_IP >> ~/.ssh/known_hosts
|
- ssh-keyscan -H $VPS_IP >> ~/.ssh/known_hosts
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import DashboardAccessGuard from '@/components/layout/DashboardAccessGuard'
|
||||||
import Sidebar from '@/components/layout/Sidebar'
|
import Sidebar from '@/components/layout/Sidebar'
|
||||||
import TopBar from '@/components/layout/TopBar'
|
import TopBar from '@/components/layout/TopBar'
|
||||||
|
|
||||||
@@ -11,7 +12,9 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
<div className="print:hidden">
|
<div className="print:hidden">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
</div>
|
</div>
|
||||||
<main className="flex-1 overflow-y-auto p-6 print:p-0 print:overflow-visible">{children}</main>
|
<main className="flex-1 overflow-y-auto p-6 print:p-0 print:overflow-visible">
|
||||||
|
<DashboardAccessGuard>{children}</DashboardAccessGuard>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { usePathname, useRouter } from 'next/navigation'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { apiFetch } from '@/lib/api'
|
||||||
|
import { toDashboardAppPath, toPublicDashboardPath } from '@/lib/dashboardPaths'
|
||||||
|
|
||||||
|
type GeneratedMenuItem = {
|
||||||
|
id: string
|
||||||
|
itemType: 'INTERNAL_PAGE' | 'EXTERNAL_LINK' | 'PARENT_MENU' | 'SECTION_LABEL' | 'DIVIDER'
|
||||||
|
routeOrUrl: string | null
|
||||||
|
children: GeneratedMenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmployeeMenuResponse = {
|
||||||
|
items: GeneratedMenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenInternalRoutes(items: GeneratedMenuItem[]): string[] {
|
||||||
|
const routes: string[] = []
|
||||||
|
|
||||||
|
const walk = (entry: GeneratedMenuItem) => {
|
||||||
|
if (entry.itemType === 'INTERNAL_PAGE' && entry.routeOrUrl) {
|
||||||
|
routes.push(entry.routeOrUrl)
|
||||||
|
}
|
||||||
|
entry.children.forEach(walk)
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach(walk)
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedRoute(currentPath: string, allowedRoutes: string[]) {
|
||||||
|
return allowedRoutes.some((route) => {
|
||||||
|
if (route === '/') return currentPath === '/'
|
||||||
|
return currentPath === route || currentPath.startsWith(`${route}/`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardAccessGuard({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname()
|
||||||
|
const router = useRouter()
|
||||||
|
const [ready, setReady] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
async function enforceAccess() {
|
||||||
|
try {
|
||||||
|
setReady(false)
|
||||||
|
const currentPath = toDashboardAppPath(pathname)
|
||||||
|
const menu = await apiFetch<EmployeeMenuResponse>('/auth/employee/menu')
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
|
const allowedRoutes = flattenInternalRoutes(menu.items)
|
||||||
|
const fallbackRoute = allowedRoutes[0] ?? '/'
|
||||||
|
|
||||||
|
if (!isAllowedRoute(currentPath, allowedRoutes)) {
|
||||||
|
router.replace(toPublicDashboardPath(fallbackRoute))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setReady(true)
|
||||||
|
} catch (error: any) {
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
|
if (error?.statusCode === 401 || error?.statusCode === 403 || error?.statusCode === 402) {
|
||||||
|
router.replace(toPublicDashboardPath('/'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not deadlock the dashboard on transient network failures.
|
||||||
|
setReady(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enforceAccess()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [pathname, router])
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[40vh] items-center justify-center">
|
||||||
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-orange-500 border-t-transparent" aria-label="Checking access" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
@@ -200,6 +200,22 @@ REGISTRY_USER=<registry-user>
|
|||||||
REGISTRY_PASSWORD=<registry-password>
|
REGISTRY_PASSWORD=<registry-password>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For the deploy job, add these GitLab CI variables as well:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VPS_IP=<server-ip-or-hostname>
|
||||||
|
VPS_USER=<ssh-user>
|
||||||
|
SSH_PRIVATE_KEY=<deployment-private-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
`SSH_PRIVATE_KEY` must be an unencrypted private key that OpenSSH can read in a non-interactive job. The pipeline accepts any of these formats:
|
||||||
|
|
||||||
|
- GitLab `File` variable containing the private key
|
||||||
|
- Standard multiline key pasted directly into the variable value
|
||||||
|
- Base64-encoded private key stored as a single line
|
||||||
|
|
||||||
|
If your key is passphrase-protected, generate a dedicated deploy key without a passphrase for CI instead of reusing an interactive workstation key.
|
||||||
|
|
||||||
The production compose file reads `APP_IMAGE` and `APP_VERSION` for pull-based deploys. The GitLab deploy job injects those values automatically and now syncs the deployment assets to the VPS before running the server-side deploy script. Production no longer depends on `git pull` during release.
|
The production compose file reads `APP_IMAGE` and `APP_VERSION` for pull-based deploys. The GitLab deploy job injects those values automatically and now syncs the deployment assets to the VPS before running the server-side deploy script. Production no longer depends on `git pull` during release.
|
||||||
|
|
||||||
The CI pipeline publishes and deploys from the GitLab default branch (`$CI_DEFAULT_BRANCH`). If you change your release branch, update the repository default branch in GitLab instead of hard-coding branch names in `.gitlab-ci.yml`.
|
The CI pipeline publishes and deploys from the GitLab default branch (`$CI_DEFAULT_BRANCH`). If you change your release branch, update the repository default branch in GitLab instead of hard-coding branch names in `.gitlab-ci.yml`.
|
||||||
|
|||||||
Reference in New Issue
Block a user