/ opencodehub / src / pages / login.astro
swadhinbiswas feat: implement air-gapped mode, UI fixes, and project management integrations · 7/9/2026
e3bd4a1
215 lines · 7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getCsrfToken } from "@/middleware/csrf";

if (Astro.locals.user) {
  return Astro.redirect("/dashboard");
}

const { token, cookie } = getCsrfToken(Astro.request);
Astro.response.headers.append("Set-Cookie", cookie);
---

<BaseLayout title="Sign in to OpenCodeHub">
  <div class="flex min-h-[calc(100vh-4rem)] items-center justify-center py-12">
    <div class="w-full max-w-md">
      <!-- Logo -->
      <div class="text-center mb-8">
        <a href="/" class="inline-flex items-center gap-2">
          <div
            class="p-2 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600"
          >
            <svg
              class="h-8 w-8 text-white"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                stroke-linecap="round"
                stroke-linejoin="round"
                stroke-width="2"
                d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
            </svg>
          </div>
          <span class="text-2xl font-bold text-white">OpenCodeHub</span>
        </a>
      </div>

      <!-- Login Card -->
      <div class="relative">
        <div
          class="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/30 via-purple-500/20 to-pink-500/30 rounded-2xl blur-sm"
        >
        </div>
        <div
          class="relative rounded-2xl border border-white/10 bg-[#0d1117]/90 backdrop-blur-xl p-8"
        >
          <div class="text-center mb-6">
            <h1 class="text-2xl font-bold text-white mb-2">Welcome back</h1>
            <p class="text-gray-500 text-sm">
              Sign in to your account to continue
            </p>
          </div>

          <form id="login-form" class="space-y-5">
            <div class="space-y-2">
              <label
                for="login"
                class="block text-sm font-medium text-gray-300"
              >
                Username or Email
              </label>
              <input
                id="login"
                name="login"
                type="text"
                placeholder="Enter your username or email"
                required
                class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all"
              />
            </div>

            <div class="space-y-2">
              <div class="flex items-center justify-between">
                <label
                  for="password"
                  class="block text-sm font-medium text-gray-300"
                >
                  Password
                </label>
                <a
                  href="/password-reset"
                  class="text-sm text-cyan-400 hover:text-cyan-300 transition-colors"
                >
                  Forgot password?
                </a>
              </div>
              <input
                id="password"
                name="password"
                type="password"
                placeholder="Enter your password"
                required
                class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all"
              />
            </div>

            <div id="totp-container" class="space-y-2 hidden">
              <label
                for="totpCode"
                class="block text-sm font-medium text-gray-300"
              >
                Two-Factor Code
              </label>
              <input
                id="totpCode"
                name="totpCode"
                type="text"
                placeholder="000 000"
                class="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all font-mono tracking-widest text-center text-lg"
                maxlength="6"
              />
            </div>

            <div
              id="error-message"
              class="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg p-3 hidden"
              role="alert"
            >
            </div>

            <input type="hidden" name="_csrf" id="csrf-token" value={token} />

            <button
              type="submit"
              id="submit-btn"
              class="w-full py-3 px-4 rounded-lg bg-gradient-to-r from-cyan-500 to-blue-600 text-white font-medium shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 transition-all focus:outline-none focus:ring-2 focus:ring-cyan-500/50"
            >
              Sign in
            </button>
          </form>



          <!-- Footer -->
          <p class="text-center text-sm text-gray-500 mt-6">
            Don't have an account?{" "}
            <a
              href="/register"
              class="text-cyan-400 hover:text-cyan-300 font-medium transition-colors"
            >
              Sign up
            </a>
          </p>
        </div>
      </div>
    </div>
  </div>
</BaseLayout>

<script>
  const form = document.getElementById("login-form") as HTMLFormElement;
  const errorMessage = document.getElementById("error-message");
  const totpContainer = document.getElementById("totp-container");
  const submitBtn = document.getElementById("submit-btn");
  const totpInput = document.getElementById("totpCode") as HTMLInputElement;

  form?.addEventListener("submit", async (e) => {
    e.preventDefault();
    errorMessage?.classList.add("hidden");
    errorMessage!.textContent = "";

    const formData = new FormData(form);
    const data = Object.fromEntries(formData);

    // Clean TOTP code
    if (data.totpCode) {
      data.totpCode = (data.totpCode as string).replace(/\s/g, "");
    }

    const originalBtnText = submitBtn!.textContent;
    submitBtn!.textContent = "Signing in...";
    submitBtn!.setAttribute("disabled", "true");

    try {
      const csrfToken = (document.getElementById("csrf-token") as HTMLInputElement)?.value || "";
      const response = await fetch("/api/auth/login", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-csrf-token": csrfToken,
        },
        body: JSON.stringify(data),
      });

      const result = await response.json();

      if (result.success) {
        if (result.data?.requiresTwoFactor) {
          // Show 2FA input
          totpContainer?.classList.remove("hidden");
          totpInput?.focus();
          submitBtn!.textContent = "Verify & Sign in";
          submitBtn!.removeAttribute("disabled");
          errorMessage?.classList.remove("hidden"); // Use error styling for info? No, let's keep it simple.
          return;
        }

        window.location.href = "/dashboard";
      } else {
        errorMessage?.classList.remove("hidden");
        errorMessage!.textContent =
          result.error?.message || "Failed to sign in";
        submitBtn!.textContent = originalBtnText;
        submitBtn!.removeAttribute("disabled");
      }
    } catch (error) {
      errorMessage?.classList.remove("hidden");
      errorMessage!.textContent = "An unexpected error occurred";
      submitBtn!.textContent = originalBtnText;
      submitBtn!.removeAttribute("disabled");
    }
  });
</script>