@lamalibre/create-portlama 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-portlama.js +15 -0
- package/package.json +50 -0
- package/scripts/bundle-vendor.js +60 -0
- package/src/index.js +484 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +136 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/summary.js +101 -0
- package/src/tasks/harden.js +302 -0
- package/src/tasks/mtls.js +195 -0
- package/src/tasks/nginx.js +184 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +434 -0
- package/vendor/panel-client/dist/assets/index-BDOylgUN.js +323 -0
- package/vendor/panel-client/dist/assets/index-BZTMcuQt.css +1 -0
- package/vendor/panel-client/dist/index.html +13 -0
- package/vendor/panel-server/package.json +31 -0
- package/vendor/panel-server/src/index.js +86 -0
- package/vendor/panel-server/src/lib/app-error.js +14 -0
- package/vendor/panel-server/src/lib/authelia.js +482 -0
- package/vendor/panel-server/src/lib/certbot.js +328 -0
- package/vendor/panel-server/src/lib/chisel.js +357 -0
- package/vendor/panel-server/src/lib/config.js +100 -0
- package/vendor/panel-server/src/lib/files.js +251 -0
- package/vendor/panel-server/src/lib/mtls.js +197 -0
- package/vendor/panel-server/src/lib/nginx.js +529 -0
- package/vendor/panel-server/src/lib/plist.js +65 -0
- package/vendor/panel-server/src/lib/services.js +128 -0
- package/vendor/panel-server/src/lib/state.js +95 -0
- package/vendor/panel-server/src/lib/system-stats.js +58 -0
- package/vendor/panel-server/src/middleware/errors.js +58 -0
- package/vendor/panel-server/src/middleware/mtls.js +30 -0
- package/vendor/panel-server/src/middleware/onboarding-guard.js +30 -0
- package/vendor/panel-server/src/routes/health.js +22 -0
- package/vendor/panel-server/src/routes/management/certs.js +225 -0
- package/vendor/panel-server/src/routes/management/logs.js +132 -0
- package/vendor/panel-server/src/routes/management/services.js +51 -0
- package/vendor/panel-server/src/routes/management/sites.js +448 -0
- package/vendor/panel-server/src/routes/management/system.js +12 -0
- package/vendor/panel-server/src/routes/management/tunnels.js +225 -0
- package/vendor/panel-server/src/routes/management/users.js +237 -0
- package/vendor/panel-server/src/routes/management.js +20 -0
- package/vendor/panel-server/src/routes/onboarding/dns.js +73 -0
- package/vendor/panel-server/src/routes/onboarding/domain.js +35 -0
- package/vendor/panel-server/src/routes/onboarding/index.js +18 -0
- package/vendor/panel-server/src/routes/onboarding/provision.js +291 -0
- package/vendor/panel-server/src/routes/onboarding/status.js +12 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:JetBrains Mono,Fira Code,ui-monospace,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{--tw-bg-opacity: 1;background-color:rgb(9 9 11 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-4{bottom:1rem}.left-4{left:1rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-5{margin-left:1.25rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-36{margin-top:9rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-96{height:24rem}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-24{width:6rem}.w-28{width:7rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.max-w-4xl{max-width:56rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.animate-fade-in{animation:fade-in .5s ease-out}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l-0{border-left-width:0px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-0{border-top-width:0px}.border-amber-500\/20{border-color:#f59e0b33}.border-amber-700{--tw-border-opacity: 1;border-color:rgb(180 83 9 / var(--tw-border-opacity, 1))}.border-cyan-400{--tw-border-opacity: 1;border-color:rgb(34 211 238 / var(--tw-border-opacity, 1))}.border-cyan-500\/20{border-color:#06b6d433}.border-green-500\/20{border-color:#22c55e33}.border-purple-500\/20{border-color:#a855f733}.border-red-500\/20{border-color:#ef444433}.border-red-700{--tw-border-opacity: 1;border-color:rgb(185 28 28 / var(--tw-border-opacity, 1))}.border-yellow-500\/20{border-color:#eab30833}.border-zinc-600{--tw-border-opacity: 1;border-color:rgb(82 82 91 / var(--tw-border-opacity, 1))}.border-zinc-700{--tw-border-opacity: 1;border-color:rgb(63 63 70 / var(--tw-border-opacity, 1))}.border-zinc-800{--tw-border-opacity: 1;border-color:rgb(39 39 42 / var(--tw-border-opacity, 1))}.border-zinc-800\/50{border-color:#27272a80}.border-t-cyan-400{--tw-border-opacity: 1;border-top-color:rgb(34 211 238 / var(--tw-border-opacity, 1))}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-600{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity, 1))}.bg-amber-900\/30{background-color:#78350f4d}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-blue-500\/20{background-color:#3b82f633}.bg-cyan-400{--tw-bg-opacity: 1;background-color:rgb(34 211 238 / var(--tw-bg-opacity, 1))}.bg-cyan-500{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity, 1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-cyan-600{--tw-bg-opacity: 1;background-color:rgb(8 145 178 / var(--tw-bg-opacity, 1))}.bg-green-400{--tw-bg-opacity: 1;background-color:rgb(74 222 128 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-500\/20{background-color:#22c55e33}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-purple-500\/10{background-color:#a855f71a}.bg-purple-500\/20{background-color:#a855f733}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-500\/20{background-color:#ef444433}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-900\/30{background-color:#7f1d1d4d}.bg-transparent{background-color:transparent}.bg-yellow-500\/10{background-color:#eab3081a}.bg-zinc-500{--tw-bg-opacity: 1;background-color:rgb(113 113 122 / var(--tw-bg-opacity, 1))}.bg-zinc-500\/20{background-color:#71717a33}.bg-zinc-600{--tw-bg-opacity: 1;background-color:rgb(82 82 91 / var(--tw-bg-opacity, 1))}.bg-zinc-700{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity, 1))}.bg-zinc-800{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1))}.bg-zinc-800\/50{background-color:#27272a80}.bg-zinc-800\/60{background-color:#27272a99}.bg-zinc-900{--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1))}.bg-zinc-950{--tw-bg-opacity: 1;background-color:rgb(9 9 11 / var(--tw-bg-opacity, 1))}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-4{padding-bottom:1rem}.pl-1{padding-left:.25rem}.pr-10{padding-right:2.5rem}.pr-4{padding-right:1rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:JetBrains Mono,Fira Code,ui-monospace,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wider{letter-spacing:.05em}.text-amber-300{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-zinc-100{--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.text-zinc-200{--tw-text-opacity: 1;color:rgb(228 228 231 / var(--tw-text-opacity, 1))}.text-zinc-300{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity, 1))}.text-zinc-400{--tw-text-opacity: 1;color:rgb(161 161 170 / var(--tw-text-opacity, 1))}.text-zinc-500{--tw-text-opacity: 1;color:rgb(113 113 122 / var(--tw-text-opacity, 1))}.text-zinc-600{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.text-zinc-900{--tw-text-opacity: 1;color:rgb(24 24 27 / var(--tw-text-opacity, 1))}.placeholder-zinc-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(113 113 122 / var(--tw-placeholder-opacity, 1))}.placeholder-zinc-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(113 113 122 / var(--tw-placeholder-opacity, 1))}.placeholder-zinc-600::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(82 82 91 / var(--tw-placeholder-opacity, 1))}.placeholder-zinc-600::placeholder{--tw-placeholder-opacity: 1;color:rgb(82 82 91 / var(--tw-placeholder-opacity, 1))}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.placeholder\:text-zinc-600::-moz-placeholder{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.placeholder\:text-zinc-600::placeholder{--tw-text-opacity: 1;color:rgb(82 82 91 / var(--tw-text-opacity, 1))}.last\:border-b-0:last-child{border-bottom-width:0px}.hover\:border-zinc-600:hover{--tw-border-opacity: 1;border-color:rgb(82 82 91 / var(--tw-border-opacity, 1))}.hover\:bg-amber-500:hover{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.hover\:bg-cyan-300:hover{--tw-bg-opacity: 1;background-color:rgb(103 232 249 / var(--tw-bg-opacity, 1))}.hover\:bg-cyan-500:hover{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity, 1))}.hover\:bg-cyan-600:hover{--tw-bg-opacity: 1;background-color:rgb(8 145 178 / var(--tw-bg-opacity, 1))}.hover\:bg-cyan-600\/20:hover{background-color:#0891b233}.hover\:bg-green-500:hover{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600\/20:hover{background-color:#dc262633}.hover\:bg-yellow-600\/20:hover{background-color:#ca8a0433}.hover\:bg-zinc-600:hover{--tw-bg-opacity: 1;background-color:rgb(82 82 91 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-700:hover{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-800:hover{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1))}.hover\:bg-zinc-800\/50:hover{background-color:#27272a80}.hover\:text-cyan-300:hover{--tw-text-opacity: 1;color:rgb(103 232 249 / var(--tw-text-opacity, 1))}.hover\:text-cyan-400:hover{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:text-yellow-300:hover{--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity, 1))}.hover\:text-zinc-100:hover{--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.hover\:text-zinc-200:hover{--tw-text-opacity: 1;color:rgb(228 228 231 / var(--tw-text-opacity, 1))}.hover\:text-zinc-300:hover{--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity, 1))}.hover\:text-zinc-400:hover{--tw-text-opacity: 1;color:rgb(161 161 170 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-cyan-400:focus{--tw-border-opacity: 1;border-color:rgb(34 211 238 / var(--tw-border-opacity, 1))}.focus\:border-cyan-500:focus{--tw-border-opacity: 1;border-color:rgb(6 182 212 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-cyan-400:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(34 211 238 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:text-zinc-500:disabled{--tw-text-opacity: 1;color:rgb(113 113 122 / var(--tw-text-opacity, 1))}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 640px){.sm\:table-cell{display:table-cell}}@media (min-width: 768px){.md\:block{display:block}.md\:table-cell{display:table-cell}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:p-8{padding:2rem}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Portlama</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-BDOylgUN.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BZTMcuQt.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body style="background-color: #09090b">
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@portlama/panel-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Portlama management panel backend",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "node --watch src/index.js | pino-pretty",
|
|
10
|
+
"build": "echo 'No build step for panel-server'",
|
|
11
|
+
"lint": "eslint ."
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@fastify/cors": "^9.0.0",
|
|
15
|
+
"@fastify/multipart": "^9.4.0",
|
|
16
|
+
"@fastify/static": "^7.0.0",
|
|
17
|
+
"@fastify/websocket": "^10.0.0",
|
|
18
|
+
"bcryptjs": "^3.0.3",
|
|
19
|
+
"execa": "^9.6.1",
|
|
20
|
+
"fastify": "^5.8.2",
|
|
21
|
+
"js-yaml": "^4.1.1",
|
|
22
|
+
"systeminformation": "^5.31.4",
|
|
23
|
+
"zod": "^3.23.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"pino-pretty": "^11.0.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import cors from '@fastify/cors';
|
|
3
|
+
import multipart from '@fastify/multipart';
|
|
4
|
+
import fastifyStatic from '@fastify/static';
|
|
5
|
+
import websocket from '@fastify/websocket';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { loadConfig } from './lib/config.js';
|
|
9
|
+
import mtlsMiddleware from './middleware/mtls.js';
|
|
10
|
+
import errorHandler from './middleware/errors.js';
|
|
11
|
+
import healthRoutes from './routes/health.js';
|
|
12
|
+
import onboardingRoutes from './routes/onboarding/index.js';
|
|
13
|
+
import managementRoutes from './routes/management.js';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const isDev = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
|
|
19
|
+
|
|
20
|
+
async function start() {
|
|
21
|
+
const config = await loadConfig();
|
|
22
|
+
|
|
23
|
+
const server = Fastify({ logger: true });
|
|
24
|
+
|
|
25
|
+
// --- Plugins ---
|
|
26
|
+
await server.register(cors, {
|
|
27
|
+
origin: config.domain
|
|
28
|
+
? `https://panel.${config.domain}`
|
|
29
|
+
: `https://${config.ip}:9292`,
|
|
30
|
+
});
|
|
31
|
+
await server.register(multipart, {
|
|
32
|
+
limits: {
|
|
33
|
+
fileSize: 50 * 1024 * 1024, // 50MB per file
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
await server.register(websocket);
|
|
37
|
+
|
|
38
|
+
// Resolve static file root for the panel client SPA
|
|
39
|
+
let staticRoot;
|
|
40
|
+
if (config.staticDir) {
|
|
41
|
+
staticRoot = config.staticDir;
|
|
42
|
+
} else if (isDev) {
|
|
43
|
+
staticRoot = path.resolve(__dirname, '..', '..', 'panel-client', 'dist');
|
|
44
|
+
} else {
|
|
45
|
+
staticRoot = path.join(config.dataDir, 'panel-client', 'dist');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await server.register(fastifyStatic, {
|
|
49
|
+
root: staticRoot,
|
|
50
|
+
prefix: '/',
|
|
51
|
+
wildcard: false,
|
|
52
|
+
decorateReply: true,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// --- Middleware ---
|
|
56
|
+
await server.register(mtlsMiddleware);
|
|
57
|
+
await server.register(errorHandler);
|
|
58
|
+
|
|
59
|
+
// --- Routes ---
|
|
60
|
+
await server.register(healthRoutes, { prefix: '/api' });
|
|
61
|
+
await server.register(onboardingRoutes, { prefix: '/api/onboarding' });
|
|
62
|
+
await server.register(managementRoutes, { prefix: '/api' });
|
|
63
|
+
|
|
64
|
+
// --- SPA fallback ---
|
|
65
|
+
server.setNotFoundHandler((request, reply) => {
|
|
66
|
+
if (request.url.startsWith('/api')) {
|
|
67
|
+
return reply.code(404).send({ error: 'Not found' });
|
|
68
|
+
}
|
|
69
|
+
return reply.sendFile('index.html');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// --- Start ---
|
|
73
|
+
await server.listen({ host: '127.0.0.1', port: 3100 });
|
|
74
|
+
|
|
75
|
+
// --- Graceful shutdown ---
|
|
76
|
+
const shutdown = async (signal) => {
|
|
77
|
+
server.log.info({ signal }, 'Received signal, shutting down gracefully');
|
|
78
|
+
await server.close();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
83
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
start();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class AppError extends Error {
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} message — human-readable error message
|
|
4
|
+
* @param {number} statusCode — HTTP status code (e.g. 400, 404, 409, 422)
|
|
5
|
+
* @param {object|null} details — optional object with additional error context
|
|
6
|
+
*/
|
|
7
|
+
constructor(message, statusCode, details = null) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'AppError';
|
|
10
|
+
this.statusCode = statusCode;
|
|
11
|
+
this.details = details;
|
|
12
|
+
this.isOperational = true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { writeFile as fsWriteFile } from 'node:fs/promises';
|
|
3
|
+
import { access, constants } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
import bcrypt from 'bcryptjs';
|
|
9
|
+
|
|
10
|
+
const AUTHELIA_BIN = '/usr/local/bin/authelia';
|
|
11
|
+
const AUTHELIA_SERVICE = 'authelia';
|
|
12
|
+
const AUTHELIA_CONFIG_DIR = '/etc/authelia';
|
|
13
|
+
const AUTHELIA_CONFIG = path.join(AUTHELIA_CONFIG_DIR, 'configuration.yml');
|
|
14
|
+
const AUTHELIA_USERS = path.join(AUTHELIA_CONFIG_DIR, 'users.yml');
|
|
15
|
+
const AUTHELIA_SECRETS = path.join(AUTHELIA_CONFIG_DIR, '.secrets.json');
|
|
16
|
+
const AUTHELIA_LOG_DIR = '/var/log/authelia';
|
|
17
|
+
const GITHUB_API = 'https://api.github.com/repos/authelia/authelia/releases/latest';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a file exists at the given path.
|
|
21
|
+
*/
|
|
22
|
+
async function fileExists(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
await access(filePath, constants.F_OK);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the currently installed Authelia version, or null if not installed.
|
|
33
|
+
*/
|
|
34
|
+
async function getInstalledVersion() {
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await execa(AUTHELIA_BIN, ['--version']);
|
|
37
|
+
return stdout.trim();
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Write content to a system path using a temp file and sudo mv.
|
|
45
|
+
*/
|
|
46
|
+
async function sudoWriteFile(destPath, content, mode = '644') {
|
|
47
|
+
const tmpFile = path.join(tmpdir(), `authelia-${crypto.randomBytes(4).toString('hex')}`);
|
|
48
|
+
await fsWriteFile(tmpFile, content, 'utf-8');
|
|
49
|
+
await execa('sudo', ['mv', tmpFile, destPath]);
|
|
50
|
+
await execa('sudo', ['chmod', mode, destPath]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Download and install the Authelia binary from GitHub releases.
|
|
55
|
+
*/
|
|
56
|
+
export async function installAuthelia() {
|
|
57
|
+
const exists = await fileExists(AUTHELIA_BIN);
|
|
58
|
+
if (exists) {
|
|
59
|
+
const version = await getInstalledVersion();
|
|
60
|
+
if (version) {
|
|
61
|
+
return { skipped: true, version };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let releaseInfo;
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await execa('curl', [
|
|
68
|
+
'-s', '-L',
|
|
69
|
+
'-H', 'Accept: application/vnd.github+json',
|
|
70
|
+
GITHUB_API,
|
|
71
|
+
]);
|
|
72
|
+
releaseInfo = JSON.parse(stdout);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Failed to fetch Authelia release info from GitHub: ${err.message}. Check internet connectivity.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (releaseInfo.message && releaseInfo.message.includes('rate limit')) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'GitHub API rate limit exceeded. Please try again later or set a GITHUB_TOKEN environment variable.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const asset = releaseInfo.assets?.find(
|
|
86
|
+
(a) => a.name.includes('linux-amd64') && a.name.endsWith('.tar.gz'),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!asset) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'Could not find linux-amd64 tarball in the latest Authelia release. Available assets: ' +
|
|
92
|
+
(releaseInfo.assets?.map((a) => a.name).join(', ') || 'none'),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const downloadUrl = asset.browser_download_url;
|
|
97
|
+
const tmpTar = path.join(tmpdir(), `authelia-${crypto.randomBytes(4).toString('hex')}.tar.gz`);
|
|
98
|
+
const tmpExtractDir = path.join(tmpdir(), `authelia-extract-${crypto.randomBytes(4).toString('hex')}`);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await execa('curl', ['-L', '-o', tmpTar, downloadUrl]);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Failed to download Authelia from ${downloadUrl}: ${err.stderr || err.message}. Check internet connectivity.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await execa('mkdir', ['-p', tmpExtractDir]);
|
|
110
|
+
await execa('tar', ['xzf', tmpTar, '-C', tmpExtractDir]);
|
|
111
|
+
|
|
112
|
+
// Find the authelia binary in extracted contents
|
|
113
|
+
const { stdout: findResult } = await execa('find', [tmpExtractDir, '-name', 'authelia', '-type', 'f']);
|
|
114
|
+
const binaryPath = findResult.trim().split('\n')[0];
|
|
115
|
+
|
|
116
|
+
if (!binaryPath) {
|
|
117
|
+
throw new Error('Could not find authelia binary in extracted archive');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await execa('sudo', ['mv', binaryPath, AUTHELIA_BIN]);
|
|
121
|
+
await execa('sudo', ['chmod', '+x', AUTHELIA_BIN]);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw new Error(`Failed to install Authelia binary: ${err.stderr || err.message}`);
|
|
124
|
+
} finally {
|
|
125
|
+
await execa('rm', ['-rf', tmpTar, tmpExtractDir]).catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const version = await getInstalledVersion();
|
|
129
|
+
if (!version) {
|
|
130
|
+
throw new Error('Authelia was installed but version check failed. The binary may be corrupted.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { installed: true, version };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Write the Authelia configuration file.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} domain - The base domain for session cookies
|
|
140
|
+
* @param {object} secrets - Object with jwtSecret, sessionSecret, storageEncryptionKey
|
|
141
|
+
*/
|
|
142
|
+
export async function writeAutheliaConfig(domain, secrets) {
|
|
143
|
+
const { jwtSecret, sessionSecret, storageEncryptionKey } = secrets;
|
|
144
|
+
|
|
145
|
+
// Create directories
|
|
146
|
+
try {
|
|
147
|
+
await execa('sudo', ['mkdir', '-p', AUTHELIA_CONFIG_DIR]);
|
|
148
|
+
await execa('sudo', ['mkdir', '-p', AUTHELIA_LOG_DIR]);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw new Error(`Failed to create Authelia directories: ${err.stderr || err.message}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const configContent = yaml.dump({
|
|
154
|
+
server: {
|
|
155
|
+
host: '127.0.0.1',
|
|
156
|
+
port: 9091,
|
|
157
|
+
},
|
|
158
|
+
log: {
|
|
159
|
+
level: 'info',
|
|
160
|
+
file_path: path.join(AUTHELIA_LOG_DIR, 'authelia.log'),
|
|
161
|
+
},
|
|
162
|
+
jwt_secret: jwtSecret,
|
|
163
|
+
authentication_backend: {
|
|
164
|
+
file: {
|
|
165
|
+
path: AUTHELIA_USERS,
|
|
166
|
+
password: {
|
|
167
|
+
algorithm: 'bcrypt',
|
|
168
|
+
bcrypt: {
|
|
169
|
+
cost: 12,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
access_control: {
|
|
175
|
+
default_policy: 'one_factor',
|
|
176
|
+
},
|
|
177
|
+
session: {
|
|
178
|
+
name: 'portlama_session',
|
|
179
|
+
secret: sessionSecret,
|
|
180
|
+
domain,
|
|
181
|
+
expiration: '12h',
|
|
182
|
+
inactivity: '2h',
|
|
183
|
+
},
|
|
184
|
+
storage: {
|
|
185
|
+
encryption_key: storageEncryptionKey,
|
|
186
|
+
local: {
|
|
187
|
+
path: path.join(AUTHELIA_CONFIG_DIR, 'db.sqlite3'),
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
notifier: {
|
|
191
|
+
filesystem: {
|
|
192
|
+
filename: path.join(AUTHELIA_CONFIG_DIR, 'notifications.txt'),
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
totp: {
|
|
196
|
+
issuer: 'Portlama',
|
|
197
|
+
period: 30,
|
|
198
|
+
digits: 6,
|
|
199
|
+
},
|
|
200
|
+
}, { lineWidth: -1 });
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await sudoWriteFile(AUTHELIA_CONFIG, configContent, '600');
|
|
204
|
+
} catch (err) {
|
|
205
|
+
throw new Error(`Failed to write Authelia configuration: ${err.stderr || err.message}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Store secrets reference
|
|
209
|
+
const secretsContent = JSON.stringify({ jwtSecret, sessionSecret, storageEncryptionKey }, null, 2) + '\n';
|
|
210
|
+
try {
|
|
211
|
+
await sudoWriteFile(AUTHELIA_SECRETS, secretsContent, '600');
|
|
212
|
+
} catch (err) {
|
|
213
|
+
throw new Error(`Failed to write Authelia secrets file: ${err.stderr || err.message}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return AUTHELIA_CONFIG;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create an Authelia user with a bcrypt-hashed password.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} username
|
|
223
|
+
* @param {string} password
|
|
224
|
+
*/
|
|
225
|
+
export async function createUser(username, password) {
|
|
226
|
+
// Hash the password using authelia CLI with bcrypt
|
|
227
|
+
let hash;
|
|
228
|
+
try {
|
|
229
|
+
const { stdout } = await execa(AUTHELIA_BIN, [
|
|
230
|
+
'crypto', 'hash', 'generate', 'bcrypt',
|
|
231
|
+
'--password', password,
|
|
232
|
+
]);
|
|
233
|
+
// The output format is typically: Digest: $2b$12$...
|
|
234
|
+
const match = stdout.match(/Digest:\s*(\$2[aby]\$\d+\$.+)/);
|
|
235
|
+
if (match) {
|
|
236
|
+
hash = match[1];
|
|
237
|
+
} else {
|
|
238
|
+
// Fallback: the whole output may be the hash
|
|
239
|
+
hash = stdout.trim();
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
throw new Error(`Failed to hash password with bcrypt: ${err.stderr || err.message}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!hash || !hash.startsWith('$2')) {
|
|
246
|
+
throw new Error(`Bcrypt hashing produced invalid output: ${hash}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Read existing users or start fresh
|
|
250
|
+
let usersData = { users: {} };
|
|
251
|
+
try {
|
|
252
|
+
const { stdout } = await execa('sudo', ['cat', AUTHELIA_USERS]);
|
|
253
|
+
const parsed = yaml.load(stdout);
|
|
254
|
+
if (parsed && parsed.users) {
|
|
255
|
+
usersData = parsed;
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// File doesn't exist or is empty — start fresh
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
usersData.users[username] = {
|
|
262
|
+
displayname: username,
|
|
263
|
+
password: hash,
|
|
264
|
+
email: `${username}@portlama.local`,
|
|
265
|
+
groups: ['admins'],
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await writeUsers(usersData);
|
|
269
|
+
|
|
270
|
+
return { username, created: true };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Read the Authelia users file and return user info (without password hashes).
|
|
275
|
+
*/
|
|
276
|
+
export async function readUsers() {
|
|
277
|
+
try {
|
|
278
|
+
const { stdout } = await execa('sudo', ['cat', AUTHELIA_USERS]);
|
|
279
|
+
const parsed = yaml.load(stdout);
|
|
280
|
+
|
|
281
|
+
if (!parsed || !parsed.users) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return Object.entries(parsed.users).map(([username, data]) => ({
|
|
286
|
+
username,
|
|
287
|
+
displayname: data.displayname || username,
|
|
288
|
+
email: data.email || '',
|
|
289
|
+
groups: data.groups || [],
|
|
290
|
+
}));
|
|
291
|
+
} catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Atomically write the Authelia users YAML file.
|
|
298
|
+
*
|
|
299
|
+
* @param {object} usersData - The full users YAML object with a users key
|
|
300
|
+
*/
|
|
301
|
+
export async function writeUsers(usersData) {
|
|
302
|
+
const yamlContent = yaml.dump(usersData, { lineWidth: -1 });
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await sudoWriteFile(AUTHELIA_USERS, yamlContent, '600');
|
|
306
|
+
} catch (err) {
|
|
307
|
+
throw new Error(`Failed to write Authelia users file: ${err.stderr || err.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Write the Authelia systemd service unit file.
|
|
313
|
+
*/
|
|
314
|
+
export async function writeAutheliaService() {
|
|
315
|
+
const serviceContent = `[Unit]
|
|
316
|
+
Description=Authelia Authentication Server
|
|
317
|
+
After=network.target
|
|
318
|
+
|
|
319
|
+
[Service]
|
|
320
|
+
Type=simple
|
|
321
|
+
User=root
|
|
322
|
+
ExecStart=/usr/local/bin/authelia --config /etc/authelia/configuration.yml
|
|
323
|
+
Restart=always
|
|
324
|
+
RestartSec=5
|
|
325
|
+
StandardOutput=journal
|
|
326
|
+
StandardError=journal
|
|
327
|
+
SyslogIdentifier=authelia
|
|
328
|
+
|
|
329
|
+
[Install]
|
|
330
|
+
WantedBy=multi-user.target
|
|
331
|
+
`;
|
|
332
|
+
|
|
333
|
+
const tmpFile = path.join(tmpdir(), `authelia-service-${crypto.randomBytes(4).toString('hex')}`);
|
|
334
|
+
await fsWriteFile(tmpFile, serviceContent, 'utf-8');
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
await execa('sudo', ['mv', tmpFile, '/etc/systemd/system/authelia.service']);
|
|
338
|
+
await execa('sudo', ['chmod', '644', '/etc/systemd/system/authelia.service']);
|
|
339
|
+
await execa('sudo', ['systemctl', 'daemon-reload']);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
throw new Error(`Failed to write Authelia service file: ${err.stderr || err.message}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return '/etc/systemd/system/authelia.service';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Enable and start the Authelia systemd service.
|
|
349
|
+
*/
|
|
350
|
+
export async function startAuthelia() {
|
|
351
|
+
try {
|
|
352
|
+
await execa('sudo', ['systemctl', 'enable', AUTHELIA_SERVICE]);
|
|
353
|
+
await execa('sudo', ['systemctl', 'start', AUTHELIA_SERVICE]);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
throw new Error(`Failed to start Authelia service: ${err.stderr || err.message}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const { stdout } = await execa('systemctl', ['is-active', AUTHELIA_SERVICE]);
|
|
362
|
+
if (stdout.trim() === 'active') {
|
|
363
|
+
return { active: true };
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
// is-active returns non-zero for inactive
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let journalOutput = '';
|
|
370
|
+
try {
|
|
371
|
+
const { stdout } = await execa('journalctl', ['-u', AUTHELIA_SERVICE, '--no-pager', '-n', '10']);
|
|
372
|
+
journalOutput = stdout;
|
|
373
|
+
} catch {
|
|
374
|
+
journalOutput = 'Could not read journal logs';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
throw new Error(`Authelia service is not active after starting. Journal output:\n${journalOutput}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Restart the Authelia service.
|
|
382
|
+
*/
|
|
383
|
+
export async function reloadAuthelia() {
|
|
384
|
+
try {
|
|
385
|
+
await execa('sudo', ['systemctl', 'restart', AUTHELIA_SERVICE]);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
throw new Error(`Failed to restart Authelia service: ${err.stderr || err.message}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const { stdout } = await execa('systemctl', ['is-active', AUTHELIA_SERVICE]);
|
|
394
|
+
if (stdout.trim() === 'active') {
|
|
395
|
+
return { active: true };
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
// is-active returns non-zero for inactive
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
throw new Error('Authelia service is not active after restart.');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Check whether the Authelia service is currently running.
|
|
406
|
+
*/
|
|
407
|
+
export async function isAutheliaRunning() {
|
|
408
|
+
try {
|
|
409
|
+
const { stdout } = await execa('systemctl', ['is-active', AUTHELIA_SERVICE]);
|
|
410
|
+
return stdout.trim() === 'active';
|
|
411
|
+
} catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Read the raw users.yml data, returning the full object including passwords.
|
|
418
|
+
* Used internally by CRUD operations that need to modify and re-write the file.
|
|
419
|
+
*
|
|
420
|
+
* @returns {object} The parsed users.yml content with a `users` key
|
|
421
|
+
* @throws {Error} If the file cannot be read or parsed
|
|
422
|
+
*/
|
|
423
|
+
export async function readUsersRaw() {
|
|
424
|
+
const { stdout } = await execa('sudo', ['cat', AUTHELIA_USERS]);
|
|
425
|
+
const parsed = yaml.load(stdout);
|
|
426
|
+
if (!parsed || !parsed.users) {
|
|
427
|
+
return { users: {} };
|
|
428
|
+
}
|
|
429
|
+
return parsed;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Hash a password with bcrypt cost factor 12.
|
|
434
|
+
*
|
|
435
|
+
* @param {string} password - The plaintext password
|
|
436
|
+
* @returns {Promise<string>} The bcrypt hash
|
|
437
|
+
*/
|
|
438
|
+
export async function hashPassword(password) {
|
|
439
|
+
return bcrypt.hash(password, 12);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Encode a Buffer as a base32 string (RFC 4648).
|
|
444
|
+
*
|
|
445
|
+
* @param {Buffer} buffer - The bytes to encode
|
|
446
|
+
* @returns {string} The base32-encoded string
|
|
447
|
+
*/
|
|
448
|
+
export function base32Encode(buffer) {
|
|
449
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
450
|
+
let bits = 0;
|
|
451
|
+
let value = 0;
|
|
452
|
+
let output = '';
|
|
453
|
+
|
|
454
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
455
|
+
value = (value << 8) | buffer[i];
|
|
456
|
+
bits += 8;
|
|
457
|
+
while (bits >= 5) {
|
|
458
|
+
output += alphabet[(value >>> (bits - 5)) & 0x1f];
|
|
459
|
+
bits -= 5;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (bits > 0) {
|
|
464
|
+
output += alphabet[(value << (5 - bits)) & 0x1f];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return output;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Generate a TOTP secret and otpauth URI for a user.
|
|
472
|
+
*
|
|
473
|
+
* @param {string} username - The username to generate TOTP for
|
|
474
|
+
* @returns {{ secret: string, uri: string }} The base32 secret and otpauth URI
|
|
475
|
+
*/
|
|
476
|
+
export function generateTotpSecret(username) {
|
|
477
|
+
const secretBytes = crypto.randomBytes(20);
|
|
478
|
+
const secret = base32Encode(secretBytes);
|
|
479
|
+
const encodedUsername = encodeURIComponent(username);
|
|
480
|
+
const uri = `otpauth://totp/Portlama:${encodedUsername}?secret=${secret}&issuer=Portlama&algorithm=SHA1&digits=6&period=30`;
|
|
481
|
+
return { secret, uri };
|
|
482
|
+
}
|