diff --git a/public/css/accessManager.css b/public/css/accessManager.css new file mode 100644 index 0000000..beca091 --- /dev/null +++ b/public/css/accessManager.css @@ -0,0 +1,105 @@ +/* Container */ +.admin-card { + max-width: 650px; + margin: 3rem auto; + padding: 2.5rem; + background-color: var(--bg-card); + border: 1px solid var(--border-medium); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + text-align: center; +} + +/* Typography */ +.admin-card h1 { + color: var(--text-heading); + font-size: 1.8rem; + margin-bottom: 0.75rem; +} + +.admin-card p { + color: var(--text-muted); + margin-bottom: 2rem; + font-size: 1rem; +} + +/* Primary Action */ +#generate-btn { + width: 100%; + padding: 14px; + background-color: var(--action-primary); + color: var(--text-white); + font-weight: 700; + font-size: 1.1rem; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease; + text-transform: uppercase; + letter-spacing: 1px; +} + +#generate-btn:hover { + background-color: var(--action-hover); +} + +/* Result Section */ +#result-container { + margin-top: 2.5rem; + display: flex; + flex-direction: column; + gap: 1.25rem; + animation: fadeIn 0.3s ease-in-out; +} + +.link-display { + padding: 12px; + background-color: var(--bg-list-item); + border: 1px solid var(--border-light-alt); + border-radius: 4px; + font-family: "Courier New", Courier, monospace; + font-size: 0.95rem; + color: var(--accent-blue-bright); + word-break: break-all; + min-height: 1.5rem; +} + +/* Copy Actions */ +.copy-group { + display: flex; + justify-content: flex-end; +} + +#copy-token-btn, +#copy-link-btn { + padding: 6px 16px; + background-color: var(--bg-header); + color: var(--text-light); + border: none; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; +} + +#copy-token-btn:hover, +#copy-link-btn:hover { + opacity: 0.9; +} + +/* Utilities */ +.hidden { + display: none !important; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/public/css/credentialManager.css b/public/css/credentialManager.css new file mode 100644 index 0000000..d4fd1a7 --- /dev/null +++ b/public/css/credentialManager.css @@ -0,0 +1,156 @@ +/* Layout & Container */ +#manager-container { + max-width: 600px; + margin: 2rem auto; + padding: 2.5rem; + background-color: var(--bg-card); + border: 1px solid var(--border-medium); + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + text-align: center; +} + +.state-section { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Typography */ +#manager-container h1 { + color: var(--text-heading); + margin-bottom: 0.5rem; + font-size: 1.75rem; +} + +#manager-container p { + color: var(--text-muted); + line-height: 1.6; +} + +/* Buttons */ +button { + cursor: pointer; + font-weight: 600; + transition: background-color 0.2s ease; + border: none; + border-radius: 4px; +} + +#reveal-btn, +#submit-token-btn { + padding: 12px 24px; + background-color: var(--action-primary); + color: var(--text-white); + font-size: 1rem; + letter-spacing: 0.5px; +} + +#reveal-btn:hover, +#submit-token-btn:hover { + background-color: var(--action-hover); +} + +.copy-btn-small { + padding: 4px 12px; + background-color: var(--bg-nav-hover); + color: var(--text-light); + font-size: 0.8rem; +} + +.copy-btn-small:hover { + background-color: var(--accent-primary); +} + +/* Manual Input Group */ +.input-group { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} + +.token-input { + flex: 1; + padding: 12px; + border: 1px solid var(--border-input); + border-radius: 4px; + background-color: var(--bg-main); + color: var(--text-main); + font-family: inherit; +} + +.token-input:focus { + outline: 2px solid var(--accent-blue); + border-color: transparent; +} + +/* Credential Display Box */ +.credential-box { + margin-top: 2rem; + padding: 1.5rem; + background-color: var(--bg-list-item); + border: 1px solid var(--border-light); + border-radius: 6px; + text-align: left; +} + +.field { + margin-bottom: 1.25rem; +} + +.field:last-child { + margin-bottom: 0; +} + +.label { + display: block; + font-size: 0.85rem; + font-weight: 700; + color: var(--text-muted-dark); + text-transform: uppercase; + margin-bottom: 0.5rem; +} + +.value-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + background-color: var(--bg-main); + padding: 8px 12px; + border: 1px solid var(--border-light-alt); + border-radius: 4px; +} + +.value { + font-family: "Courier New", Courier, monospace; + font-weight: 600; + color: var(--accent-blue); + word-break: break-all; +} + +/* Status & Utilities */ +.status { + font-weight: 600; + color: var(--status-success); + margin-bottom: 1rem; +} + +.error-text { + color: var(--status-error); + font-size: 0.9rem; + margin-top: -0.5rem; +} + +.warning { + margin-top: 1.5rem; + padding: 1rem; + background-color: var(--bg-tag); + color: var(--accent-tag); + border-radius: 4px; + font-size: 0.9rem; + font-style: italic; +} + +.hidden { + display: none !important; +} diff --git a/public/css/styles.css b/public/css/styles.css index 95ca2f0..c0ce02d 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1,120 +1,8 @@ -:root { - /* Backgrounds */ - --bg-body: #f8f9fa; - --bg-main: #ffffff; - --bg-header: #2c3e50; - --bg-footer: #34495e; - --bg-nav-hover: #34495e; - --bg-card: #ffffff; - --bg-tag: #e3f2fd; - --bg-list-item: #f8fafc; - --bg-hover: #e9ecef; - --bg-code: #2d2d2d; - --bg-code-inline: #f0f0f0; - - /* Text Colors */ - --text-main: #212529; - --text-muted: #6c757d; - --text-light: #ecf0f1; - --text-footer-p: #bdc3c7; - --text-article: #444444; - --text-heading: #2d3748; - --text-p-alt: #4a5568; - --text-heading: #2d3748; - --text-heading-dark: #333333; - --text-muted-dark: #495057; - --text-muted-light: #999999; - --text-bold: #222222; - --text-italic: #555555; - --text-blockquote: #666677; - --text-code-inline: #c7254e; - --text-code-block: #cccccc; - - /* Accents & Links */ - --accent-primary: #2c3e50; - --accent-blue: #1a73e8; - --accent-blue-bright: #007bff; - --accent-tag: #1976d2; - --link-footer: #9bd3ff; - --link-classic: #0366d6; - --link-sitemap: #007acc; - --link-markdown: #667eea; - --link-markdown-hover: #764ba2; - --gradient-start: #667eea; - --gradient-end: #764ba2; - - /* Borders & Dividers */ - --border-light: #e0e0e0; - --border-medium: #dee2e6; - --border-markdown: #e2e8f0; - --border-grey: #cccccc; - --border-grey-dark: #bbbbbb; - --border-light-alt: #dddddd; - --border-input: #dee2e6; - --border-tag: #bbdefb; - --border-markdown: #e2e8f0; - --hr-color: #cbd5e0; - --border-subtle: #e9ecef; - --border-button: #888888; - - /* Status & Action Colors */ - --status-error: #d33333; - --status-error-alt: #dc3545; - --status-success: #28a745; - --action-primary: #2980b9; - --action-hover: #1f598a; - --action-alt: #007acc; - --action-alt-hover: #005fa3; - - /* Additional Backgrounds */ - --bg-subtle: #f5f7fa; - --bg-neutral: #eeeeee; - --bg-docs: #fdfdfd; - --bg-docs-th: #f9fafb; - --bg-docs-card: #fefefe; - --bg-sitemap: #f9f9f9; - --bg-pagination: #f5f5f5; - --bg-light-accent: #ecf0f1; - - /* Additional Text Colors */ - --text-docs-h1: #1a1a1a; - --text-docs-h2: #374151; - --text-docs-muted: #4b5563; - --text-hover-dark: #1a242f; - --text-muted-alt: #777777; - --text-placeholder: #666666; - - /* Additional Borders */ - --border-docs: #e5e7eb; - - /* Additional Mapping */ - --text-white: #ffffff; - --text-black: #000000; - --text-docs-strong: #1f2937; - --bg-docs-inline: #f1f3f4; - --bg-logs-hover: #e9e9e9; - - /* Docs & Layout Extras */ - --bg-docs-inline: #f1f3f4; - --text-docs-strong: #1f2937; - --border-docs: #e5e7eb; - - /* Log & Interaction Extras */ - --bg-logs-hover: #e9e9e9; - --text-black: #000000; - --text-white: #ffffff; - - /* Deep Dive & Gradient Specifics */ - --deep-dive-bg: #0a192f; - --deep-dive-glow: #00ced1; - --deep-dive-mid-1: #1a2744; - --deep-dive-mid-2: #0f1b36; - --gradient-start-trans: #667eea15; - --gradient-end-trans: #764ba215; -} +@import "./theme.css"; /* Base styles */ -html, body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +html, +body { + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; background-color: var(--bg-body); color: var(--text-main); min-height: 100vh; @@ -128,7 +16,11 @@ } .pattern-dots { position: relative; - background-image: radial-gradient(circle, rgba(0,0,0,0.07) 1px, transparent 1px); + background-image: radial-gradient( + circle, + rgba(0, 0, 0, 0.07) 1px, + transparent 1px + ); background-size: 15px 15px; z-index: 0; } @@ -137,7 +29,11 @@ pointer-events: none; position: absolute; inset: 0; - background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 90%); + background: linear-gradient( + to bottom, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 1) 90% + ); } .pattern-dots > * { position: relative; @@ -147,7 +43,7 @@ display: none; } .inline { - display: inline + display: inline; } footer { background-color: var(--bg-footer); @@ -155,7 +51,7 @@ padding: 2rem 0; text-align: center; border-top: none; - box-shadow: 0 -2px 5px rgba(0,0,0,0.1); + box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); font-size: 0.9rem; } footer .container { @@ -199,7 +95,6 @@ } footer nav p { margin: 0; - } footer .social-contact { display: flex; @@ -231,17 +126,19 @@ color: var(--text-muted-alt); } footer small { - color: var(--text-placeholder); font-size: 10px; opacity: 0.3; + color: var(--text-placeholder); + font-size: 10px; + opacity: 0.3; } footer small a { - color: inherit + color: inherit; } /* Header */ header#site-header { background-color: var(--bg-header); color: var(--text-light); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - position:relative; + position: relative; z-index: 10; } header#site-header .container { @@ -273,7 +170,8 @@ position: relative; display: inline-block; } -header#site-header .site-title, a { +header#site-header .site-title, +a { color: inherit; text-decoration: none; position: relative; @@ -295,18 +193,17 @@ transform: scaleX(1); } .pattern-lambda { - background-image: - repeating-linear-gradient( - 45deg, - transparent, - transparent 50px, - rgba(236, 240, 241, 0.08) 50px, - rgba(236, 240, 241, 0.08) 52px - ); + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent 50px, + rgba(236, 240, 241, 0.08) 50px, + rgba(236, 240, 241, 0.08) 52px + ); position: relative; } .pattern-lambda::before { - content: ' '; + content: " "; position: absolute; top: 0; left: 0; @@ -325,7 +222,7 @@ background-color: var(--bg-main); padding: 2rem; border-radius: 6px; - box-shadow: 0 2px 6px rgba(0,0,0,0.05); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); } main h1 { font-size: 1.5rem; @@ -400,10 +297,10 @@ position: absolute; background-color: white; min-width: 160px; - box-shadow: 0px 8px 16px rgba(0,0,0,0.2); + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.2); - flex-direction: column; - white-space: nowrap; + flex-direction: column; + white-space: nowrap; left: 0; top: 100%; } @@ -452,7 +349,7 @@ top: 0; left: 100%; /* shift nested submenu to the right */ margin-left: 0.2rem; /* small gap */ - min-width: 160px; + min-width: 160px; z-index: 12; } nav.site-nav .dropdown-content .dropdown:hover > .dropdown-content { @@ -521,7 +418,7 @@ color: var(--accent-blue); text-decoration: underline; } -.sidebar .menu-year { +.sidebar .menu-year { margin-bottom: 1em; } .sidebar .month-label { @@ -663,16 +560,16 @@ padding: 1rem; margin-bottom: 1.5rem; } - + .toc-header { font-size: 1rem; } - + .toc a { padding: 0.4rem 0.5rem; font-size: 0.9rem; } - + .toc li.h3 { margin-left: 0.5rem; } @@ -723,7 +620,7 @@ float: none; padding: 0.5rem; border-bottom: 1px solid var(--text-article); - + text-align: left; } header#site-header nav.site-nav.hide { @@ -791,149 +688,168 @@ user-select: none; } .deep-dive-enhanced { - background: linear-gradient(135deg, #0A192F 0%, var(--deep-dive-mid-1) 30%, var(--deep-dive-mid-2) 70%, var(--deep-dive-bg) 100%); - border-radius: 8px; - color: var(--deep-dive-glow); - font-weight: 700; - position: relative; - overflow: hidden; - box-shadow: - 0 0 8px rgba(0, 206, 209, 0.4), - 0 0 20px rgba(0, 206, 209, 0.15), - inset 0 1px 0 rgba(255, 255, 255, 0.1), - inset 0 0 20px rgba(0, 206, 209, 0.05); - transition: all 0.3s ease; - border: 1px solid rgba(0, 206, 209, 0.2); + background: linear-gradient( + 135deg, + #0a192f 0%, + var(--deep-dive-mid-1) 30%, + var(--deep-dive-mid-2) 70%, + var(--deep-dive-bg) 100% + ); + border-radius: 8px; + color: var(--deep-dive-glow); + font-weight: 700; + position: relative; + overflow: hidden; + box-shadow: + 0 0 8px rgba(0, 206, 209, 0.4), + 0 0 20px rgba(0, 206, 209, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1), + inset 0 0 20px rgba(0, 206, 209, 0.05); + transition: all 0.3s ease; + border: 1px solid rgba(0, 206, 209, 0.2); } .deep-dive-enhanced:hover { - transform: translateY(-1px); - box-shadow: - 0 0 12px rgba(0, 206, 209, 0.6), - 0 0 30px rgba(0, 206, 209, 0.25), - 0 2px 8px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.15), - inset 0 0 25px rgba(0, 206, 209, 0.08); - border-color: rgba(0, 206, 209, 0.4); + transform: translateY(-1px); + box-shadow: + 0 0 12px rgba(0, 206, 209, 0.6), + 0 0 30px rgba(0, 206, 209, 0.25), + 0 2px 8px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.15), + inset 0 0 25px rgba(0, 206, 209, 0.08); + border-color: rgba(0, 206, 209, 0.4); } .deep-dive-enhanced::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(45deg, transparent, rgba(236, 240, 241, 0.08), transparent); - animation: shimmer 4s; + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 45deg, + transparent, + rgba(236, 240, 241, 0.08), + transparent + ); + animation: shimmer 4s; } .deep-dive-enhanced::after { - content: ''; - position: absolute; - top: 2px; - left: 2px; - right: 2px; - bottom: 2px; - background: linear-gradient(135deg, rgba(0, 206, 209, 0.03) 0%, transparent 50%, rgba(0, 206, 209, 0.03) 100%); - border-radius: 6px; - pointer-events: none; + content: ""; + position: absolute; + top: 2px; + left: 2px; + right: 2px; + bottom: 2px; + background: linear-gradient( + 135deg, + rgba(0, 206, 209, 0.03) 0%, + transparent 50%, + rgba(0, 206, 209, 0.03) 100% + ); + border-radius: 6px; + pointer-events: none; } .icon-text { - position: relative; - z-index: 2; - text-shadow: 0 0 8px rgba(0, 206, 209, 0.5); - animation: pulse 4s ease-in-out infinite; + position: relative; + z-index: 2; + text-shadow: 0 0 8px rgba(0, 206, 209, 0.5); + animation: pulse 4s ease-in-out infinite; } @keyframes shimmer { - 0% { - left: -100%; - } - 100% { - left: 100%; - } + 0% { + left: -100%; + } + 100% { + left: 100%; + } } @keyframes pulse { - 0%, 100% { - text-shadow: 0 0 8px rgba(0, 206, 209, 0.5); - } - 50% { - text-shadow: 0 0 12px rgba(0, 206, 209, 0.8), 0 0 20px rgba(0, 206, 209, 0.3); - } + 0%, + 100% { + text-shadow: 0 0 8px rgba(0, 206, 209, 0.5); + } + 50% { + text-shadow: + 0 0 12px rgba(0, 206, 209, 0.8), + 0 0 20px rgba(0, 206, 209, 0.3); + } } /* Floating particles effect */ .particles { - position: absolute; - width: 100%; - height: 100%; - pointer-events: none; - overflow: hidden; - border-radius: 12px; + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; + overflow: hidden; + border-radius: 12px; } .particle { - position: absolute; - width: 2px; - height: 2px; - background: var(--deep-dive-glow); - border-radius: 50%; - opacity: 0.6; - animation: float 6s linear infinite; + position: absolute; + width: 2px; + height: 2px; + background: var(--deep-dive-glow); + border-radius: 50%; + opacity: 0.6; + animation: float 6s linear infinite; } .particle:nth-child(1) { - left: 20%; - animation-delay: -1s; - animation-duration: 5s; + left: 20%; + animation-delay: -1s; + animation-duration: 5s; } .particle:nth-child(2) { - left: 50%; - animation-delay: -2s; - animation-duration: 7s; + left: 50%; + animation-delay: -2s; + animation-duration: 7s; } .particle:nth-child(3) { - left: 80%; - animation-delay: -3s; - animation-duration: 6s; + left: 80%; + animation-delay: -3s; + animation-duration: 6s; } @keyframes float { - 0% { - transform: translateY(70px) scale(0); - opacity: 0; - } - 10% { - opacity: 0.6; - } - 90% { - opacity: 0.6; - } - 100% { - transform: translateY(-10px) scale(1); - opacity: 0; - } + 0% { + transform: translateY(70px) scale(0); + opacity: 0; + } + 10% { + opacity: 0.6; + } + 90% { + opacity: 0.6; + } + 100% { + transform: translateY(-10px) scale(1); + opacity: 0; + } } /* Corner accents */ .corner-accent { - position: absolute; - width: 6px; - height: 1px; - background: rgba(236, 240, 241, 0.3); - opacity: 0.8; + position: absolute; + width: 6px; + height: 1px; + background: rgba(236, 240, 241, 0.3); + opacity: 0.8; } .corner-accent.top-left { - top: 4px; - left: 4px; - transform: rotate(45deg); + top: 4px; + left: 4px; + transform: rotate(45deg); } .corner-accent.bottom-right { - bottom: 4px; - right: 4px; - transform: rotate(45deg); + bottom: 4px; + right: 4px; + transform: rotate(45deg); } .corner-accent.top-right { - top: 4px; - right: 4px; - transform: rotate(-45deg); + top: 4px; + right: 4px; + transform: rotate(-45deg); } .corner-accent.bottom-left { - bottom: 4px; - left: 4px; - transform: rotate(-45deg); + bottom: 4px; + left: 4px; + transform: rotate(-45deg); } @media print { @page { diff --git a/public/css/theme.css b/public/css/theme.css new file mode 100644 index 0000000..5165d36 --- /dev/null +++ b/public/css/theme.css @@ -0,0 +1,114 @@ +:root { + /* Backgrounds */ + --bg-body: #f8f9fa; + --bg-main: #ffffff; + --bg-header: #2c3e50; + --bg-footer: #34495e; + --bg-nav-hover: #34495e; + --bg-card: #ffffff; + --bg-tag: #e3f2fd; + --bg-list-item: #f8fafc; + --bg-hover: #e9ecef; + --bg-code: #2d2d2d; + --bg-code-inline: #f0f0f0; + + /* Text Colors */ + --text-main: #212529; + --text-muted: #6c757d; + --text-light: #ecf0f1; + --text-footer-p: #bdc3c7; + --text-article: #444444; + --text-heading: #2d3748; + --text-p-alt: #4a5568; + --text-heading: #2d3748; + --text-heading-dark: #333333; + --text-muted-dark: #495057; + --text-muted-light: #999999; + --text-bold: #222222; + --text-italic: #555555; + --text-blockquote: #666677; + --text-code-inline: #c7254e; + --text-code-block: #cccccc; + + /* Accents & Links */ + --accent-primary: #2c3e50; + --accent-blue: #1a73e8; + --accent-blue-bright: #007bff; + --accent-tag: #1976d2; + --link-footer: #9bd3ff; + --link-classic: #0366d6; + --link-sitemap: #007acc; + --link-markdown: #667eea; + --link-markdown-hover: #764ba2; + --gradient-start: #667eea; + --gradient-end: #764ba2; + + /* Borders & Dividers */ + --border-light: #e0e0e0; + --border-medium: #dee2e6; + --border-markdown: #e2e8f0; + --border-grey: #cccccc; + --border-grey-dark: #bbbbbb; + --border-light-alt: #dddddd; + --border-input: #dee2e6; + --border-tag: #bbdefb; + --border-markdown: #e2e8f0; + --hr-color: #cbd5e0; + --border-subtle: #e9ecef; + --border-button: #888888; + + /* Status & Action Colors */ + --status-error: #d33333; + --status-error-alt: #dc3545; + --status-success: #28a745; + --action-primary: #2980b9; + --action-hover: #1f598a; + --action-alt: #007acc; + --action-alt-hover: #005fa3; + + /* Additional Backgrounds */ + --bg-subtle: #f5f7fa; + --bg-neutral: #eeeeee; + --bg-docs: #fdfdfd; + --bg-docs-th: #f9fafb; + --bg-docs-card: #fefefe; + --bg-sitemap: #f9f9f9; + --bg-pagination: #f5f5f5; + --bg-light-accent: #ecf0f1; + + /* Additional Text Colors */ + --text-docs-h1: #1a1a1a; + --text-docs-h2: #374151; + --text-docs-muted: #4b5563; + --text-hover-dark: #1a242f; + --text-muted-alt: #777777; + --text-placeholder: #666666; + + /* Additional Borders */ + --border-docs: #e5e7eb; + + /* Additional Mapping */ + --text-white: #ffffff; + --text-black: #000000; + --text-docs-strong: #1f2937; + --bg-docs-inline: #f1f3f4; + --bg-logs-hover: #e9e9e9; + + /* Docs & Layout Extras */ + --bg-docs-inline: #f1f3f4; + --text-docs-strong: #1f2937; + --border-docs: #e5e7eb; + + /* Log & Interaction Extras */ + --bg-logs-hover: #e9e9e9; + --text-black: #000000; + --text-white: #ffffff; + + /* Deep Dive & Gradient Specifics */ + --deep-dive-bg: #0a192f; + --deep-dive-glow: #00ced1; + --deep-dive-mid-1: #1a2744; + --deep-dive-mid-2: #0f1b36; + --gradient-start-trans: #667eea15; + --gradient-end-trans: #764ba215; +} diff --git a/public/js/accessManager.js b/public/js/accessManager.js new file mode 100644 index 0000000..6a45833 --- /dev/null +++ b/public/js/accessManager.js @@ -0,0 +1,72 @@ +import { bindCopyAction } from "./copyUtils.js"; + +/** + * Handles communication with the admin access endpoint. + * Credentials included to satisfy Nginx auth_request proxy. + */ +async function fetchAccessLink() { + const endpoint = "https://access.jasonpoage.com/admin/generate-link"; + const response = await fetch(endpoint, { + method: "GET", + credentials: "include", + }); + + return await processResponse(response); +} + +/** + * Validates and parses the response from the generator endpoint. + * Logic extracted to maintain flat nesting. + */ +async function processResponse(response) { + if (!response.ok) { + throw new Error(`Execution Failed: ${response.status}`); + } + return response.json(); +} + + +/** + * Updates the DOM with the generated URI and initializes copy logic. + */ +function updateUI(data) { + const token = document.getElementById("token-output"); + const output = document.getElementById("link-output"); + const container = document.getElementById("result-container"); + const copyBtn = document.getElementById("copy-link-btn"); + const copyTokenBtn = document.getElementById("copy-token-btn"); + + // Dynamically construct URL based on environment + const fullUrl = `${window.location.origin}/guest-access/${data.access_token}`; + + output.innerText = fullUrl; + token.innerText = data.access_token; + + container.classList.remove("hidden"); + + bindCopyAction(copyBtn, output); + bindCopyAction(copyTokenBtn, token); +} + +/** + * Orchestrates the generation workflow. + */ +function handleGeneration() { + const btn = document.getElementById("generate-btn"); + btn.disabled = true; + + fetchAccessLink() + .then(updateUI) + .catch((err) => { + document.getElementById("link-output").innerText = + `Error: ${err.message}`; + document.getElementById("result-container").classList.remove("hidden"); + }) + .finally(() => { + btn.disabled = false; + }); +} + +document + .getElementById("generate-btn") + .addEventListener("click", handleGeneration); diff --git a/public/js/copyUtils.js b/public/js/copyUtils.js new file mode 100644 index 0000000..041f9f3 --- /dev/null +++ b/public/js/copyUtils.js @@ -0,0 +1,16 @@ +/** + * Binds a clipboard copy event to a trigger element. + * @param {HTMLElement} trigger - The element that initiates the copy. + * @param {HTMLElement} source - The element containing the text to copy. + */ +export function bindCopyAction(trigger, source) { + trigger.addEventListener("click", function () { + const textToCopy = source.innerText || source.value; + + navigator.clipboard.writeText(textToCopy).then(() => { + const originalText = trigger.innerText; + trigger.innerText = "COPIED!"; + setTimeout(() => (trigger.innerText = originalText), 2000); + }); + }); +} diff --git a/public/js/credentialManager.js b/public/js/credentialManager.js new file mode 100644 index 0000000..227ce8a --- /dev/null +++ b/public/js/credentialManager.js @@ -0,0 +1,132 @@ +/** + * Manages the lifecycle of recruiter credentials. + * Ensures credentials are only generated on explicit user intent. + */ +class CredentialManager { + constructor() { + this.container = document.getElementById("manager-container"); + this.token = this.container ? this.container.dataset.token : null; + } + + /** + * Initializes event listeners for the reveal action. + */ + init() { + const self = this; + const revealBtn = document.getElementById("reveal-btn"); + + if (revealBtn) { + revealBtn.addEventListener("click", () => self.handleReveal()); + } + } + + submitToken() { + const submitBtn = document.getElementById("submit-token-btn"); + const manualInput = document.getElementById("token-input"); + + if (manualBtn) { + manualBtn.addEventListener("click", () => { + const tokenValue = manualInput.value.trim(); + if (tokenValue) { + // Update container attribute for use by existing logic + container.setAttribute("data-token", tokenValue); + + // Trigger the reveal logic defined in your reveal-btn listener + revealCredentials(); + } + }); + } + } + + /** + * Orchestrates the reveal process via POST request. + */ + async handleReveal() { + const self = this; + const btn = document.getElementById("reveal-btn"); + + self._toggleLoading(btn, true); + + try { + const response = await fetch( + `https://access.jasonpoage.com/access/${self.token}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + const data = await self._processResponse(response); + self._displayCredentials(data); + } catch (err) { + self._handleRevealError(err.message); + } finally { + self._toggleLoading(btn, false); + } + } + + /** + * Validates response status and parses JSON payload. + */ + async _processResponse(response) { + const self = this; + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || `Error: ${response.status}`); + } + return response.json(); + } + + /** + * Updates DOM elements with credential data and transitions visibility. + */ + _displayCredentials(data) { + const self = this; + const revealSection = document.getElementById("reveal-section"); + const credSection = document.getElementById("credential-section"); + + document.getElementById("reveal-message").innerText = data.message; + document.getElementById("username").innerText = data.username; + document.getElementById("password").innerText = data.password; + + revealSection.classList.add("hidden"); + credSection.classList.remove("hidden"); + + self._initCopyButtons(); + } + + /** + * Binds clipboard actions using dynamic import in-place. + */ + async _initCopyButtons() { + const self = this; + const userTrigger = document.getElementById("copy-user-btn"); + const userSource = document.getElementById("username"); + const passTrigger = document.getElementById("copy-pass-btn"); + const passSource = document.getElementById("password"); + + const copyUtils = await import("./copyUtils.js"); + copyUtils.bindCopyAction(userTrigger, userSource); + copyUtils.bindCopyAction(passTrigger, passSource); + } + + /** + * UI state helper for the reveal button. + */ + _toggleLoading(element, isLoading) { + const self = this; + element.disabled = isLoading; + element.innerText = isLoading ? "GENERATING..." : "GET CREDENTIALS"; + } + + /** + * Renders error state in the reveal section. + */ + _handleRevealError(msg) { + const self = this; + const messageEl = document.getElementById("reveal-section"); + messageEl.innerHTML = `

Access Denied

${msg}

`; + } +} + +const manager = new CredentialManager(); +manager.init(); diff --git a/src/controllers/accessController.js b/src/controllers/accessController.js new file mode 100644 index 0000000..912e552 --- /dev/null +++ b/src/controllers/accessController.js @@ -0,0 +1,22 @@ +/** + * Handles the GET request from a recruiter clicking the unique link. + */ +exports.handleAccessConsumption = async (req, res, next) => { + const { token } = req.params; + + try { + res.renderWithBaseContext("pages/credentials.handlebars", { + title: "Portfolio Access", + token, + }); + } catch (err) { + next(err); + } +}; + +exports.renderPortal = async (req, res, next) => { + res.renderWithBaseContext("admin-pages/accessManager.handlebars", { + title: "Access Management", + // Base context handles user/session data + }); +}; diff --git a/src/routes/guestAccessRouter.js b/src/routes/guestAccessRouter.js new file mode 100644 index 0000000..5b739fe --- /dev/null +++ b/src/routes/guestAccessRouter.js @@ -0,0 +1,21 @@ +const express = require("express"); +const router = express.Router(); + +const logEvent = require("../middleware/analytics.js"); +const securedMiddleware = require("./secured"); + +const { + handleAccessConsumption, + renderPortal, +} = require("../controllers/accessController"); + +router.get( + "/access/manager", + logEvent("admin"), + securedMiddleware, + renderPortal, +); + +router.get("/guest-access/:token", logEvent("admin"), handleAccessConsumption); + +module.exports = router; diff --git a/src/routes/index.js b/src/routes/index.js index c1eb186..a69c013 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -14,6 +14,7 @@ const sitemap = require("./sitemap"); const { blogPost, blogIndex } = require("../controllers/blogControllers"); const pages = require("./pages"); +const guestAccess = require("./guestAccessRouter"); const projects = require("./projects"); const docs = require("./docs"); const rssFeedController = require("../controllers/rssFeedController"); @@ -44,6 +45,7 @@ router.get("/error", errorPage); // Landing page after error is logged router.use(admin); +router.use(guestAccess); router.use( "/static", diff --git a/src/views/admin-pages/accessManager.handlebars b/src/views/admin-pages/accessManager.handlebars new file mode 100644 index 0000000..ccea69d --- /dev/null +++ b/src/views/admin-pages/accessManager.handlebars @@ -0,0 +1,26 @@ +{{#section "scripts"}} + +{{/section}} + +{{#section "styles"}} + +{{/section}} + +
+

Recruiter Access Generator

+

Generate a one-time unique URI for prospective employers.

+ + + + +
+ diff --git a/src/views/pages/credentials.handlebars b/src/views/pages/credentials.handlebars new file mode 100644 index 0000000..6200448 --- /dev/null +++ b/src/views/pages/credentials.handlebars @@ -0,0 +1,56 @@ +{{#section "scripts"}} + +{{/section}} + +{{#section "styles"}} + +{{/section}} + +
+ {{!-- Initial State: Reveal Button --}} + {{#if token}} + {{!-- State: Token Provided via URL --}} +
+

Portfolio Access

+

Click the button below to generate your one-time credentials.

+ +
+ {{else}} + {{!-- State: Manual Token Entry Required --}} +
+

Manual Access

+

No access token detected. Please enter your token manually to continue.

+
+ + +
+ +
+ {{/if}} + + {{!-- Result State: Credentials (Hidden Initially) --}} + +