This commit is contained in:
Stanley 2025-12-16 09:52:18 +07:00
parent dbeb74692d
commit 717669245d
8 changed files with 241 additions and 30 deletions

View File

@ -1,11 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="Login1.css">
<title>Space Odyssey</title>
</head>
<body>
<div class="stars"></div>
<div class="neon-grid"></div>
@ -14,66 +16,149 @@
<div class="left-panel">
<div class="title-container">
<h1 class="main-title">SPACE<br>ODYSSEY</h1>
<div class="subtitle">Initializing Spaceship</div>
<div class="subtitle" id="subtitle">Initializing Spaceship</div>
</div>
</div>
<div class="right-panel">
<div class="form-box">
<div class="form-content">
<h2 class="form-title">Access Terminal</h2>
<!-- box login-->
<label for="username">Username</label>
<input type="text" id="username" placeholder="Enter callsign...">
<h2 class="form-title" id="formTitle">Access Terminal</h2>
<label for="username" id="loginLabel">Username</label>
<input type="text" id="username" placeholder="Enter callsign..." autocomplete="username">
<label for="password">Password</label>
<input type="password" id="password" placeholder="Enter access code...">
<input type="password" id="password" placeholder="Enter access code..."
autocomplete="current-password">
<div class="button-group">
<button class="btn-send" onclick="registerUser()">Connect</button>
<button class="btn-send" type="button" id="connectBtn">Connect</button>
<div class="signup-text">
<!-- link belm selsai-->
Don't have a terminal? <a href="#">Sign up</a>
<span id="toggleText">Don't have a terminal?</span> <a href="#" id="toggleLink">Sign up</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="user-list" id="userList"></div>
<script>
let mode = 'login';
let userCounter = 0;
function registerUser() {
const username = document.getElementById('username').value.trim();
const password = document.getElementById('password').value;
if (!username || !password) {
alert('ya begitulah');
return;
}
const els = {
subtitle: document.getElementById('subtitle'),
formTitle: document.getElementById('formTitle'),
loginLabel: document.getElementById('loginLabel'),
username: document.getElementById('username'),
password: document.getElementById('password'),
connectBtn: document.getElementById('connectBtn'),
toggleText: document.getElementById('toggleText'),
toggleLink: document.getElementById('toggleLink'),
userList: document.getElementById('userList'),
};
function showToast(htmlText) {
const userId = 'user_' + userCounter++;
const userCard = document.createElement('div');
userCard.className = 'user-card';
userCard.id = userId;
userCard.innerHTML = `
<div class="user-name">✓ You have logged on as <strong>${username}</strong></div>
`;
document.getElementById('userList').appendChild(userCard);
setTimeout(() => {
userCard.classList.add('usercardfade-out');
userCard.innerHTML = `<div class="user-name">${htmlText}</div>`;
els.userList.appendChild(userCard);
setTimeout(() => {
userCard.remove();
}, 500);
userCard.classList.add('usercardfade-out');
setTimeout(() => userCard.remove(), 500);
}, 3000);
}
}, 3000);
function setMode(nextMode) {
mode = nextMode;
};
if (mode === 'login') {
els.formTitle.textContent = 'Access Terminal';
els.subtitle.textContent = 'Initializing Spaceship';
els.loginLabel.textContent = 'Username';
els.username.placeholder = 'Enter callsign...';
els.password.autocomplete = 'current-password';
els.connectBtn.textContent = 'Connect';
els.toggleText.textContent = "Don't have a terminal?";
els.toggleLink.textContent = 'Sign up';
} else {
els.formTitle.textContent = 'Create Terminal';
els.subtitle.textContent = 'Registering Pilot';
els.loginLabel.textContent = 'Username';
els.username.placeholder = 'Choose callsign...';
els.password.autocomplete = 'new-password';
els.connectBtn.textContent = 'Register';
els.toggleText.textContent = 'Already have a terminal?';
els.toggleLink.textContent = 'Log in';
}
}
async function postJson(url, payload) {
const res = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
let data = null;
try { data = await res.json(); } catch (_) { }
if (!res.ok) {
const msg = (data && data.error) ? data.error : `Request failed (${res.status})`;
throw new Error(msg);
}
return data;
}
async function submitAuth() {
const username = els.username.value.trim();
const password = els.password.value;
if (!username || !password) {
showToast('✗ Please enter <strong>username</strong> and <strong>password</strong>.');
return;
}
els.connectBtn.disabled = true;
try {
if (mode === 'login') {
const out = await postJson('./api/login.php', { login: username, password });
showToast(`✓ You have logged on as <strong>${out.user.username}</strong>`);
setTimeout(() => { window.location.href = './Main.html'; }, 700);
} else {
const out = await postJson('./api/register.php', { username, password });
showToast(`✓ Terminal created for <strong>${out.user.username}</strong>`);
setTimeout(() => { window.location.href = './Main.html'; }, 700);
}
} catch (e) {
showToast(`✗ <strong>${e.message}</strong>`);
} finally {
els.connectBtn.disabled = false;
}
}
els.connectBtn.addEventListener('click', submitAuth);
els.toggleLink.addEventListener('click', (e) => {
e.preventDefault();
setMode(mode === 'login' ? 'register' : 'login');
els.password.value = '';
els.password.focus();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') submitAuth();
});
setMode('login');
</script>
</body>
</html>

34
api/config.php Normal file
View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
session_start();
header('Content-Type: application/json; charset=utf-8');
$DB_HOST = '127.0.0.1';
$DB_NAME = 'space_odyssey';
$DB_USER = 'root';
$DB_PASS = '';
function json_out(int $status, array $data): void {
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$dsn = "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4";
$pdo = new PDO($dsn, $DB_USER, $DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (Throwable $e) {
json_out(500, ['ok' => false, 'error' => 'DB connection failed']);
}
function current_user(PDO $pdo): ?array {
if (!isset($_SESSION['user_id'])) return null;
$stmt = $pdo->prepare('SELECT id, username, email, created_at FROM users WHERE id = ? LIMIT 1');
$stmt->execute([(int)$_SESSION['user_id']]);
$u = $stmt->fetch();
return $u ?: null;
}

33
api/login.php Normal file
View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
require __DIR__ . '/config.php';
$data = json_decode(file_get_contents('php://input') ?: '[]', true);
if (!is_array($data)) json_out(400, ['ok' => false, 'error' => 'Invalid JSON']);
$login = isset($data['login']) ? trim((string)$data['login']) : '';
$password = isset($data['password']) ? (string)$data['password'] : '';
if ($login === '' || $password === '') {
json_out(400, ['ok' => false, 'error' => 'Missing login or password']);
}
$stmt = $pdo->prepare('SELECT id, username, email, password_hash, created_at FROM users WHERE username = ? OR email = ? LIMIT 1');
$stmt->execute([$login, $login]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, (string)$user['password_hash'])) {
json_out(401, ['ok' => false, 'error' => 'Invalid credentials']);
}
$_SESSION['user_id'] = (int)$user['id'];
json_out(200, [
'ok' => true,
'user' => [
'id' => (int)$user['id'],
'username' => (string)$user['username'],
'email' => $user['email'],
'created_at' => $user['created_at'],
]
]);

12
api/logout.php Normal file
View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
require __DIR__ . '/config.php';
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], (bool)$p['secure'], (bool)$p['httponly']);
}
session_destroy();
json_out(200, ['ok' => true]);

6
api/me.php Normal file
View File

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
require __DIR__ . '/config.php';
$u = current_user($pdo);
json_out(200, ['ok' => true, 'user' => $u]);

41
api/register.php Normal file
View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
require __DIR__ . '/config.php';
$data = json_decode(file_get_contents('php://input') ?: '[]', true);
if (!is_array($data)) json_out(400, ['ok' => false, 'error' => 'Invalid JSON']);
$username = isset($data['username']) ? trim((string)$data['username']) : '';
$email = isset($data['email']) ? trim((string)$data['email']) : '';
$password = isset($data['password']) ? (string)$data['password'] : '';
if ($username === '' || strlen($username) < 3 || strlen($username) > 32) {
json_out(400, ['ok' => false, 'error' => 'Username must be 3-32 chars']);
}
if (!preg_match('/^[A-Za-z0-9_]+$/', $username)) {
json_out(400, ['ok' => false, 'error' => 'Username must be letters/numbers/_ only']);
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
json_out(400, ['ok' => false, 'error' => 'Invalid email']);
}
if (strlen($password) < 6) {
json_out(400, ['ok' => false, 'error' => 'Password must be at least 6 chars']);
}
$hash = password_hash($password, PASSWORD_DEFAULT);
try {
$stmt = $pdo->prepare('INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$username, ($email === '' ? null : $email), $hash]);
$_SESSION['user_id'] = (int)$pdo->lastInsertId();
$u = current_user($pdo);
json_out(201, ['ok' => true, 'user' => $u]);
} catch (Throwable $e) {
$msg = $e->getMessage();
if (stripos($msg, 'Duplicate') !== false || stripos($msg, 'uq_') !== false) {
json_out(409, ['ok' => false, 'error' => 'Username or email already used']);
}
json_out(500, ['ok' => false, 'error' => 'Register failed']);
}

BIN
img/module_table_bottom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

BIN
img/module_table_top.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B