login-page.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. /**
  2. * The one page the worker renders itself.
  3. *
  4. * It is inline rather than a static asset because it is the only thing served
  5. * without a session — keeping it here means the asset directory can stay
  6. * entirely behind the gate, with no "is this file public?" judgement calls.
  7. */
  8. function escapeHtml(value: string): string {
  9. return value
  10. .replace(/&/g, '&')
  11. .replace(/</g, '&lt;')
  12. .replace(/>/g, '&gt;')
  13. .replace(/"/g, '&quot;')
  14. .replace(/'/g, '&#39;');
  15. }
  16. export interface LoginPageOptions {
  17. /** Path to return to after a successful sign-in. Already validated same-origin. */
  18. next: string;
  19. /** Shown above the form when a previous attempt failed. */
  20. error?: string;
  21. /** CSP nonce for the inline stylesheet. */
  22. nonce: string;
  23. }
  24. export function renderLoginPage({ next, error, nonce }: LoginPageOptions): string {
  25. const errorBlock = error ? `\n <p class="error" role="alert">${escapeHtml(error)}</p>` : '';
  26. return `<!doctype html>
  27. <html lang="en">
  28. <head>
  29. <meta charset="utf-8" />
  30. <meta name="viewport" content="width=device-width, initial-scale=1" />
  31. <title>Sign in — codegraph telemetry</title>
  32. <style nonce="${nonce}">
  33. :root {
  34. --paper: #f7f6f2;
  35. --ink: #16150f;
  36. --oxblood: #7a201a;
  37. --rule: #d8d5cb;
  38. }
  39. * { box-sizing: border-box; }
  40. body {
  41. margin: 0;
  42. min-height: 100vh;
  43. display: flex;
  44. align-items: center;
  45. justify-content: center;
  46. padding: 24px;
  47. background: var(--paper);
  48. color: var(--ink);
  49. font-family: 'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  50. font-size: 16px;
  51. line-height: 1.5;
  52. }
  53. main { width: 100%; max-width: 380px; }
  54. h1 { margin: 0 0 4px; font-size: 22px; font-weight: 600; }
  55. .subtitle { margin: 0 0 24px; color: #56534a; }
  56. hr { border: 0; border-top: 1px solid var(--rule); margin: 0 0 24px; }
  57. label { display: block; margin-bottom: 6px; }
  58. input[type='password'] {
  59. width: 100%;
  60. padding: 9px 10px;
  61. font: inherit;
  62. color: var(--ink);
  63. background: #fff;
  64. border: 1px solid var(--rule);
  65. border-radius: 0;
  66. }
  67. input[type='password']:focus {
  68. outline: 2px solid var(--oxblood);
  69. outline-offset: -2px;
  70. border-color: var(--oxblood);
  71. }
  72. button {
  73. margin-top: 16px;
  74. width: 100%;
  75. padding: 10px 12px;
  76. font: inherit;
  77. color: var(--paper);
  78. background: var(--oxblood);
  79. border: 1px solid var(--oxblood);
  80. border-radius: 0;
  81. cursor: pointer;
  82. }
  83. button:hover { background: #5f1914; border-color: #5f1914; }
  84. .error {
  85. margin: 0 0 16px;
  86. padding: 9px 10px;
  87. color: var(--oxblood);
  88. background: #fff;
  89. border: 1px solid var(--oxblood);
  90. }
  91. .footnote { margin: 24px 0 0; color: #56534a; font-size: 14px; }
  92. </style>
  93. </head>
  94. <body>
  95. <main>
  96. <h1>codegraph telemetry</h1>
  97. <p class="subtitle">This dashboard is private. Enter the shared password to continue.</p>
  98. <hr />${errorBlock}
  99. <form method="post" action="/login">
  100. <input type="hidden" name="next" value="${escapeHtml(next)}" />
  101. <label for="password">Password</label>
  102. <input
  103. id="password"
  104. name="password"
  105. type="password"
  106. autocomplete="current-password"
  107. required
  108. autofocus
  109. />
  110. <button type="submit">Sign in</button>
  111. </form>
  112. <p class="footnote">You stay signed in on this browser for a year.</p>
  113. </main>
  114. </body>
  115. </html>
  116. `;
  117. }