summaryrefslogtreecommitdiff
path: root/web/templates/login.html
blob: 19bce009d73a4b3e396c133454c10390517ce20a (plain)
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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login - Personal Dashboard</title>
    <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
    <link rel="stylesheet" href="/static/css/output.css">
    <style>
        .text-shadow-sm { text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
    </style>
</head>
<body class="min-h-screen flex items-center justify-center bg-slate-950 text-slate-200">
    <div class="bg-overlay" style="background-image: url('https://picsum.photos/1920/1080?random=login');"></div>
    <div class="w-full max-w-md p-6 sm:p-8 relative z-10">
        <div class="card shadow-2xl">
            <h1 class="text-3xl font-light text-white text-center mb-10 tracking-tight text-shadow-sm">Personal Dashboard</h1>

            {{if .Error}}
            <div class="mb-6 p-4 bg-red-900/50 border border-red-500/30 rounded-lg text-red-300 text-sm">
                {{.Error}}
            </div>
            {{end}}

            <form method="POST" action="/login" class="space-y-6" autocomplete="on">
                <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
                <div>
                    <label for="username" class="block text-sm font-medium text-white/70 mb-2">
                        Username
                    </label>
                    <input
                        type="text"
                        id="username"
                        name="username"
                        autocomplete="username"
                        required
                        autofocus
                        class="w-full px-4 py-3 bg-black/40 border border-white/20 rounded-lg text-white placeholder-white/50 focus:ring-2 focus:ring-white/30 focus:border-white/30 transition-colors"
                        placeholder="Enter your username">
                </div>

                <div>
                    <label for="password" class="block text-sm font-medium text-white/70 mb-2">
                        Password
                    </label>
                    <input
                        type="password"
                        id="password"
                        name="password"
                        autocomplete="current-password"
                        required
                        class="w-full px-4 py-3 bg-black/40 border border-white/20 rounded-lg text-white placeholder-white/50 focus:ring-2 focus:ring-white/30 focus:border-white/30 transition-colors"
                        placeholder="Enter your password">
                </div>

                <button
                    type="submit"
                    class="w-full bg-white/20 hover:bg-white/30 text-white font-medium py-3 px-4 rounded-lg transition-colors">
                    Sign In
                </button>
            </form>

            {{if .WebAuthnEnabled}}
            <div id="passkey-section" class="mt-6 pt-6 border-t border-white/20" style="display: none;">
                <button
                    id="passkey-login-btn"
                    type="button"
                    class="w-full bg-white/10 hover:bg-white/20 text-white font-medium py-3 px-4 rounded-lg transition-colors border border-white/20">
                    Sign in with Passkey
                </button>
                <p id="passkey-error" class="mt-2 text-red-300 text-sm" style="display: none;"></p>
            </div>
            <script>
            (function() {
                if (!window.PublicKeyCredential) return;
                document.getElementById('passkey-section').style.display = 'block';

                document.getElementById('passkey-login-btn').addEventListener('click', async function() {
                    const btn = this;
                    const errEl = document.getElementById('passkey-error');
                    errEl.style.display = 'none';
                    btn.disabled = true;
                    btn.textContent = 'Waiting for passkey...';

                    try {
                        const csrfToken = document.querySelector('input[name="csrf_token"]').value;
                        const beginResp = await fetch('/passkeys/login/begin', {
                            method: 'POST',
                            headers: {'X-CSRF-Token': csrfToken}
                        });
                        if (!beginResp.ok) throw new Error('Failed to start login');
                        const options = await beginResp.json();

                        options.publicKey.challenge = base64urlToBuffer(options.publicKey.challenge);
                        if (options.publicKey.allowCredentials) {
                            options.publicKey.allowCredentials = options.publicKey.allowCredentials.map(c => ({
                                ...c, id: base64urlToBuffer(c.id)
                            }));
                        }

                        const assertion = await navigator.credentials.get(options);

                        const body = JSON.stringify({
                            id: assertion.id,
                            rawId: bufferToBase64url(assertion.rawId),
                            type: assertion.type,
                            response: {
                                authenticatorData: bufferToBase64url(assertion.response.authenticatorData),
                                clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON),
                                signature: bufferToBase64url(assertion.response.signature),
                                userHandle: assertion.response.userHandle ? bufferToBase64url(assertion.response.userHandle) : ''
                            }
                        });

                        const finishResp = await fetch('/passkeys/login/finish', {
                            method: 'POST',
                            headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken},
                            body: body
                        });
                        const result = await finishResp.json();
                        if (!finishResp.ok) throw new Error(result.error || 'Login failed');

                        window.location.href = result.redirect || '/';
                    } catch (e) {
                        if (e.name === 'NotAllowedError') {
                            errEl.textContent = 'Passkey request was cancelled.';
                        } else {
                            errEl.textContent = e.message || 'Passkey login failed.';
                        }
                        errEl.style.display = 'block';
                        btn.disabled = false;
                        btn.textContent = 'Sign in with Passkey';
                    }
                });

                function base64urlToBuffer(base64url) {
                    const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
                    const pad = base64.length % 4 === 0 ? '' : '='.repeat(4 - (base64.length % 4));
                    const binary = atob(base64 + pad);
                    return Uint8Array.from(binary, c => c.charCodeAt(0)).buffer;
                }

                function bufferToBase64url(buffer) {
                    const bytes = new Uint8Array(buffer);
                    let binary = '';
                    for (const b of bytes) binary += String.fromCharCode(b);
                    return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
                }
            })();
            </script>
            {{end}}
        </div>
    </div>
</body>
</html>