diff --git a/config/sql/users.ts b/config/sql/users.ts index 560b906..c6eab77 100644 --- a/config/sql/users.ts +++ b/config/sql/users.ts @@ -70,6 +70,10 @@ export function isValidUsername(username: string): { valid: boolean; error?: string; } { + if (!username) { + return { valid: false, error: "" }; + } + if (username.length < userNameRestrictions.length.min) { return { valid: false, error: "Username is too short" }; } @@ -89,6 +93,10 @@ export function isValidPassword(password: string): { valid: boolean; error?: string; } { + if (!password) { + return { valid: false, error: "" }; + } + if (password.length < passwordRestrictions.length.min) { return { valid: false, @@ -117,6 +125,10 @@ export function isValidEmail(email: string): { valid: boolean; error?: string; } { + if (!email) { + return { valid: false, error: "" }; + } + if (!emailRestrictions.regex.test(email)) { return { valid: false, error: "Invalid email address" }; } @@ -128,6 +140,10 @@ export function isValidInvite(invite: string): { valid: boolean; error?: string; } { + if (!invite) { + return { valid: false, error: "" }; + } + if (invite.length < inviteRestrictions.min) { return { valid: false, diff --git a/public/css/auth/login.css b/public/css/auth/login.css index 4de920e..84f7c26 100644 --- a/public/css/auth/login.css +++ b/public/css/auth/login.css @@ -9,13 +9,11 @@ .content { border: 1px solid var(--border); background-color: var(--background-secondary); - display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem; - width: clamp(200px, 50%, 300px); } @@ -25,3 +23,137 @@ align-items: center; justify-content: center; } + +.login-container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + background: linear-gradient(135deg, rgba(31 30 30 / 90%) 0%, rgba(45 45 45 / 90%) 100%); +} + +.login-logo { + margin-bottom: 2rem; + text-align: center; +} + +.login-logo h1 { + font-size: 2.5rem; + font-weight: bold; + color: var(--text); + margin: 0; +} + +.login-logo p { + color: var(--text-secondary); + margin-top: 0.5rem; +} + +.login-card { + width: 100%; + max-width: 400px; + border-radius: 8px; + box-shadow: var(--card-shadow); + background-color: var(--background-secondary); + overflow: hidden; + animation: fade-in 0.5s ease; +} + +.login-header { + padding: 1.5rem; + background-color: rgba(0 0 0 / 10%); + text-align: center; +} + +.login-header h2 { + margin: 0; + color: var(--text); + font-size: 1.5rem; +} + +.login-form { + padding: 1.5rem; +} + +.login-form form { + display: flex; + flex-direction: column; + width: 100%; +} + +.login-register { + text-align: center; + margin-top: 1.5rem; +} + +.form-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 1rem; +} + +.form-footer a { + color: var(--accent); + text-decoration: none; + font-size: 0.9rem; +} + +.form-footer a:hover { + text-decoration: underline; +} + +.form-footer label { + display: flex; + align-items: center; + gap: 0.25rem; + white-space: nowrap; +} + +.login-form button { + margin-top: 1rem; + width: 100%; +} + +.error-message { + color: var(--error); + background-color: rgb(237 66 69 / 10%); + border-left: 4px solid var(--error); + padding: 0.75rem; + margin-bottom: 1rem; + border-radius: 4px; + display: none; +} + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(-20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.register-link { + color: #57f287; + text-decoration: none; + font-weight: bold; +} + +.register-link:hover { + text-decoration: underline; +} + +@media (width <= 480px) { + .login-card { + max-width: 100%; + } + + .login-logo h1 { + font-size: 2rem; + } +} diff --git a/public/css/global.css b/public/css/global.css index f550c18..f2df698 100644 --- a/public/css/global.css +++ b/public/css/global.css @@ -1,40 +1,112 @@ [data-theme="dark"] { - --background: rgb(31, 30, 30); - --background-secondary: rgb(45, 45, 45); - --border: rgb(31, 30, 30); - --text: rgb(255, 255, 255); - --text-secondary: rgb(255, 255, 255); + --background: rgb(31 30 30); + --background-secondary: rgb(45 45 45); + --border: rgb(70 70 70); + --text: rgb(255 255 255); + --text-secondary: rgb(200 200 200); + --accent: rgb(88 101 242); + --accent-hover: rgb(71 82 196); + --error: rgb(237 66 69); + --success: rgb(87 242 135); + --shadow: rgb(0 0 0 / 20%); + --card-shadow: 0 2px 10px 0 rgb(0 0 0 / 20%); + --input-background: rgb(55 55 55); } body { - font-family: "Ubuntu", sans-serif; - + font-family: Ubuntu, sans-serif; margin: 0; padding: 0; box-sizing: border-box; font-size: 16px; - background-color: var(--background); + color: var(--text); } /* Fonts */ @font-face { - font-family: "Ubuntu"; + font-family: Ubuntu; src: url("/public/assets/fonts/Ubuntu/Ubuntu-Regular.ttf") format("truetype"); font-weight: normal; font-style: normal; } @font-face { - font-family: "Ubuntu Bold"; + font-family: Ubuntu Bold; src: url("/public/assets/fonts/Ubuntu/Ubuntu-Bold.ttf") format("truetype"); font-weight: bold; font-style: normal; } @font-face { - font-family: "Fira Code"; + font-family: Fira Code; src: url("/public/assets/fonts/Fira_code/FiraCode-Regular.ttf") format("truetype"); font-weight: normal; font-style: normal; } + +/* Utility classes */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Form elements */ +input, button, textarea, select { + font-family: inherit; + font-size: 1rem; + border-radius: 4px; + transition: all 0.2s ease; +} + +button, .button { + cursor: pointer; + padding: 0.75rem 1.5rem; + border: none; + background-color: var(--accent); + color: white; + font-weight: bold; + border-radius: 4px; + transition: background-color 0.2s ease; +} + +button:hover, .button:hover { + background-color: var(--accent-hover); +} + +input, textarea, select { + padding: 0.75rem; + border: 1px solid var(--border); + background-color: var(--input-background); + color: var(--text); + width: 100%; + box-sizing: border-box; +} + +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px rgb(88 101 242 / 30%); +} + +.form-group { + margin-bottom: 1.5rem; + width: 100%; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: bold; + color: var(--text-secondary); +} + +/* Card style */ +.card { + background-color: var(--background-secondary); + border-radius: 8px; + box-shadow: var(--card-shadow); + padding: 1.5rem; + overflow: hidden; +} diff --git a/public/js/auth/login.js b/public/js/auth/login.js new file mode 100644 index 0000000..b95dec2 --- /dev/null +++ b/public/js/auth/login.js @@ -0,0 +1,37 @@ +const loginForm = document.getElementById("login-form"); +const errorMessage = document.getElementById("error-message"); + +if (loginForm) { + loginForm.addEventListener("submit", async (e) => { + e.preventDefault(); + + const email = document.getElementById("email").value; + const password = document.getElementById("password").value; + + try { + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "same-origin", + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (data.success) { + window.location.href = "/"; + } else { + errorMessage.style.display = "block"; + errorMessage.textContent = + data.error || + "Invalid email or password. Please try again."; + } + } catch (error) { + console.error("Login error:", error); + errorMessage.style.display = "block"; + errorMessage.textContent = "An error occurred. Please try again."; + } + }); +} diff --git a/src/routes/api/auth/login.ts b/src/routes/api/auth/login.ts index 8cfa4fd..6e1fa3f 100644 --- a/src/routes/api/auth/login.ts +++ b/src/routes/api/auth/login.ts @@ -59,11 +59,16 @@ async function handler( } const errors: string[] = []; + const validations: UserValidation[] = [ - { check: isValidUsername(username), field: "Username" }, - { check: isValidEmail(email), field: "Email" }, - { check: isValidPassword(password), field: "Password" }, - ]; + username + ? { check: isValidUsername(username), field: "Username" } + : null, + email ? { check: isValidEmail(email), field: "Email" } : null, + password + ? { check: isValidPassword(password), field: "Password" } + : null, + ].filter(Boolean) as UserValidation[]; validations.forEach(({ check }: UserValidation): void => { if (!check.valid && check.error) { @@ -71,6 +76,10 @@ async function handler( } }); + if (!username && !email) { + errors.push("Either a username or an email is required."); + } + if (errors.length > 0) { return Response.json( { @@ -86,11 +95,11 @@ async function handler( let user: User | null = null; try { - user = await reservation` + [user] = await reservation` SELECT * FROM users WHERE (username = ${username} OR email = ${email}) LIMIT 1; - `.then((rows: User[]): User | null => rows[0]); + `; if (!user) { await bunPassword.verify("fake", await bunPassword.hash("fake")); diff --git a/src/routes/auth/login.ts b/src/routes/auth/login.ts index b7b4e68..74cf330 100644 --- a/src/routes/auth/login.ts +++ b/src/routes/auth/login.ts @@ -8,10 +8,12 @@ const routeDef: RouteDef = { }; async function handler(): Promise { + const instanceName: string = + (await getSetting("instance_name")) || "Unnamed Instance"; + const ejsTemplateData: EjsTemplateData = { - title: "Hello, World!", - instance_name: - (await getSetting("instance_name")) || "Unnamed Instance", + title: `Login - ${instanceName}`, + instance_name: instanceName, }; return await renderEjsTemplate("auth/login", ejsTemplateData); diff --git a/src/views/auth/login.ejs b/src/views/auth/login.ejs index b6f4f15..252f932 100644 --- a/src/views/auth/login.ejs +++ b/src/views/auth/login.ejs @@ -1,28 +1,56 @@ + - <%- include("../global", { styles: ["auth/login"], scripts: [] }) %> + <%- include("../global", { styles: ["auth/login"], scripts: ["auth/login"] }) %> + -
-
-

<%= instance_name %>

+