Auth System
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
-- phpMyAdmin SQL Dump
|
||||||
|
-- version 5.2.2
|
||||||
|
-- https://www.phpmyadmin.net/
|
||||||
|
--
|
||||||
|
-- Host: dns.burnednodes.ge
|
||||||
|
-- Generation Time: Apr 01, 2026 at 03:48 AM
|
||||||
|
-- Server version: 10.6.22-MariaDB-0ubuntu0.22.04.1
|
||||||
|
-- PHP Version: 8.4.18
|
||||||
|
|
||||||
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
|
SET time_zone = "+00:00";
|
||||||
|
|
||||||
|
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||||
|
/*!40101 SET NAMES utf8mb4 */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Database: `YabosRageMPCore`
|
||||||
|
--
|
||||||
|
|
||||||
|
-- --------------------------------------------------------
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `users`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`email` varchar(255) NOT NULL,
|
||||||
|
`password` varchar(255) NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`name` varchar(24) NOT NULL DEFAULT '',
|
||||||
|
`lastname` varchar(24) NOT NULL DEFAULT '',
|
||||||
|
`hardwareid` varchar(128) DEFAULT '',
|
||||||
|
`ip` varchar(64) DEFAULT '',
|
||||||
|
`adminLvl` int(11) DEFAULT 0,
|
||||||
|
`social` varchar(128) DEFAULT '',
|
||||||
|
`socialid` varchar(128) DEFAULT ''
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `users`
|
||||||
|
--
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD PRIMARY KEY (`id`),
|
||||||
|
ADD UNIQUE KEY `email` (`email`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `users`
|
||||||
|
--
|
||||||
|
ALTER TABLE `users`
|
||||||
|
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||||
|
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||||
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
+18
-2
@@ -1,13 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<Chat />
|
<Auth v-if="showAuth" />
|
||||||
|
<Chat v-if="!showAuth" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { Rpc } from '@entityseven/rage-fw-rpc';
|
||||||
|
import Auth from './components/Auth.vue';
|
||||||
import Chat from './components/Chat.vue';
|
import Chat from './components/Chat.vue';
|
||||||
|
|
||||||
|
const appRpc = new Rpc({ forceBrowserDevMode: typeof (window as any).mp === 'undefined' });
|
||||||
|
const showAuth = ref(true);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
appRpc.register('app:showGame', () => {
|
||||||
|
showAuth.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
appRpc.register('app:showAuth', () => {
|
||||||
|
showAuth.value = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Reset everything */
|
|
||||||
body, html {
|
body, html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -0,0 +1,527 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-overlay" v-if="visible">
|
||||||
|
<div class="auth-container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="auth-header">
|
||||||
|
<div class="logo-line"></div>
|
||||||
|
<h1 class="auth-title">YABOS<span class="accent">RAGEMP</span>CORE</h1>
|
||||||
|
<div class="logo-line"></div>
|
||||||
|
</div>
|
||||||
|
<p class="auth-subtitle">{{ isLogin ? 'Sign in to continue' : 'Create a new account' }}</p>
|
||||||
|
|
||||||
|
<!-- Tab Switcher -->
|
||||||
|
<div class="auth-tabs">
|
||||||
|
<button :class="{ active: isLogin }" @click="switchTab(true)">SIGN IN</button>
|
||||||
|
<button :class="{ active: !isLogin }" @click="switchTab(false)">REGISTER</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form -->
|
||||||
|
<form @submit.prevent="handleSubmit" class="auth-form">
|
||||||
|
<!-- Name / Lastname (Register only) -->
|
||||||
|
<div class="name-row" v-if="!isLogin">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="firstName"
|
||||||
|
@input="formatName('first')"
|
||||||
|
placeholder="First name"
|
||||||
|
required
|
||||||
|
maxlength="24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="lastName"
|
||||||
|
@input="formatName('last')"
|
||||||
|
placeholder="Last name"
|
||||||
|
required
|
||||||
|
maxlength="24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
v-model="email"
|
||||||
|
placeholder="Email address"
|
||||||
|
required
|
||||||
|
autocomplete="email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
v-model="password"
|
||||||
|
placeholder="Password"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
<button type="button" class="toggle-pw" @click="showPassword = !showPassword" tabindex="-1">
|
||||||
|
{{ showPassword ? 'HIDE' : 'SHOW' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group" v-if="!isLogin">
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
v-model="confirmPassword"
|
||||||
|
placeholder="Confirm password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remember Me -->
|
||||||
|
<div class="auth-options" v-if="isLogin">
|
||||||
|
<label class="remember-label">
|
||||||
|
<input type="checkbox" v-model="rememberMe" />
|
||||||
|
<span class="cb"></span>
|
||||||
|
Remember credentials
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<div v-if="errorMsg" class="msg msg-error">{{ errorMsg }}</div>
|
||||||
|
<div v-if="successMsg" class="msg msg-success">{{ successMsg }}</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<button type="submit" class="auth-submit" :disabled="loading">
|
||||||
|
<span v-if="!loading">{{ isLogin ? 'SIGN IN' : 'CREATE ACCOUNT' }}</span>
|
||||||
|
<span v-else class="dots">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="auth-footer">Press <kbd>`</kbd> to toggle cursor</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { Rpc } from '@entityseven/rage-fw-rpc';
|
||||||
|
|
||||||
|
const authRpc = new Rpc({ forceBrowserDevMode: typeof (window as any).mp === 'undefined' });
|
||||||
|
|
||||||
|
const visible = ref(true);
|
||||||
|
const isLogin = ref(true);
|
||||||
|
const email = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const confirmPassword = ref('');
|
||||||
|
const firstName = ref('');
|
||||||
|
const lastName = ref('');
|
||||||
|
const rememberMe = ref(true);
|
||||||
|
const showPassword = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMsg = ref('');
|
||||||
|
const successMsg = ref('');
|
||||||
|
|
||||||
|
const NAME_REGEX = /^[A-Za-z]+$/;
|
||||||
|
|
||||||
|
const formatName = (field: 'first' | 'last') => {
|
||||||
|
const target = field === 'first' ? firstName : lastName;
|
||||||
|
// Strip anything that isn't an English letter
|
||||||
|
let val = target.value.replace(/[^A-Za-z]/g, '');
|
||||||
|
// Force first letter uppercase, rest lowercase
|
||||||
|
if (val.length > 0) {
|
||||||
|
val = val.charAt(0).toUpperCase() + val.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
target.value = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateName = (name: string): boolean => {
|
||||||
|
return name.length >= 2 && name.length <= 24 && NAME_REGEX.test(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchTab = (login: boolean) => {
|
||||||
|
isLogin.value = login;
|
||||||
|
errorMsg.value = '';
|
||||||
|
successMsg.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
errorMsg.value = '';
|
||||||
|
successMsg.value = '';
|
||||||
|
|
||||||
|
if (!email.value || !password.value) {
|
||||||
|
errorMsg.value = 'Please fill in all fields.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLogin.value) {
|
||||||
|
if (!firstName.value || !lastName.value) {
|
||||||
|
errorMsg.value = 'Please enter your first and last name.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validateName(firstName.value)) {
|
||||||
|
errorMsg.value = 'First name must be 2-24 English letters only.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validateName(lastName.value)) {
|
||||||
|
errorMsg.value = 'Last name must be 2-24 English letters only.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.value !== confirmPassword.value) {
|
||||||
|
errorMsg.value = 'Passwords do not match.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.value.length < 6) {
|
||||||
|
errorMsg.value = 'Password must be at least 6 characters.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result: any;
|
||||||
|
|
||||||
|
if (isLogin.value) {
|
||||||
|
result = await authRpc.callClient('auth:login', [email.value, password.value]);
|
||||||
|
} else {
|
||||||
|
result = await authRpc.callClient('auth:register', [
|
||||||
|
email.value, password.value, firstName.value, lastName.value
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
successMsg.value = isLogin.value ? 'Welcome back.' : 'Account created successfully.';
|
||||||
|
|
||||||
|
if (rememberMe.value && isLogin.value) {
|
||||||
|
authRpc.callClient('client:saveCredentials', [email.value, password.value]).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
visible.value = false;
|
||||||
|
}, 600);
|
||||||
|
} else {
|
||||||
|
errorMsg.value = (result && result.error) || 'An error occurred. Please try again.';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errorMsg.value = 'Connection error. Please try again.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
authRpc.register('auth:show', () => {
|
||||||
|
visible.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
authRpc.register('auth:hide', () => {
|
||||||
|
visible.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
authRpc.callClient('client:loadCredentials', []).then((saved: any) => {
|
||||||
|
if (saved && saved.email) {
|
||||||
|
email.value = saved.email;
|
||||||
|
password.value = saved.password || '';
|
||||||
|
rememberMe.value = true;
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');
|
||||||
|
|
||||||
|
.auth-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container {
|
||||||
|
width: 380px;
|
||||||
|
background: rgba(12, 12, 16, 0.85);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 42px 36px 32px;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
letter-spacing: 3px;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent { color: #c9a84c; }
|
||||||
|
|
||||||
|
.auth-subtitle {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 28px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs button {
|
||||||
|
flex: 1;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 0 0 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs button.active {
|
||||||
|
color: #c9a84c;
|
||||||
|
border-bottom-color: #c9a84c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-tabs button:hover:not(.active) {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-row .input-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 13px 16px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input:focus {
|
||||||
|
border-color: rgba(201, 168, 76, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-pw {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-pw:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-options { margin: 4px 0; }
|
||||||
|
|
||||||
|
.remember-label {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-label input { display: none; }
|
||||||
|
|
||||||
|
.cb {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-label input:checked + .cb {
|
||||||
|
background: #c9a84c;
|
||||||
|
border-color: #c9a84c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-label input:checked + .cb::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 4px;
|
||||||
|
width: 4px;
|
||||||
|
height: 7px;
|
||||||
|
border: solid rgba(0,0,0,0.8);
|
||||||
|
border-width: 0 1.5px 1.5px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
animation: msgFade 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes msgFade {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-error {
|
||||||
|
background: rgba(180, 50, 50, 0.15);
|
||||||
|
border: 1px solid rgba(180, 50, 50, 0.25);
|
||||||
|
color: #e06060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg-success {
|
||||||
|
background: rgba(50, 160, 80, 0.1);
|
||||||
|
border: 1px solid rgba(50, 160, 80, 0.2);
|
||||||
|
color: #5cc97a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-submit {
|
||||||
|
width: 100%;
|
||||||
|
background: #c9a84c;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 13px;
|
||||||
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-submit:hover:not(:disabled) { background: #d4b45a; }
|
||||||
|
.auth-submit:active:not(:disabled) { background: #b89840; }
|
||||||
|
.auth-submit:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dots span {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: dotPulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dots span:nth-child(2) { animation-delay: 0.15s; }
|
||||||
|
.dots span:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
|
||||||
|
@keyframes dotPulse {
|
||||||
|
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||||||
|
40% { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.15);
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0 0;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer kbd {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+169
-12
@@ -2,8 +2,12 @@ import { Rpc } from '@entityseven/rage-fw-rpc';
|
|||||||
|
|
||||||
const rpc = new Rpc();
|
const rpc = new Rpc();
|
||||||
let browser: BrowserMp | null = null;
|
let browser: BrowserMp | null = null;
|
||||||
|
let cursorVisible = true;
|
||||||
|
let adminLevel = 0;
|
||||||
|
let noclipActive = false;
|
||||||
|
let noclipCamera: CameraMp | null = null;
|
||||||
|
|
||||||
// Initialize CEF via standard RAGE MP event to bootstrap the browser for RPC
|
// Initialize CEF via standard RAGE MP event
|
||||||
mp.events.add('client:initCef', (cefUrl: string, isDebug: boolean) => {
|
mp.events.add('client:initCef', (cefUrl: string, isDebug: boolean) => {
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
mp.gui.chat.push(`[Client] Initializing CEF: ${cefUrl}`);
|
mp.gui.chat.push(`[Client] Initializing CEF: ${cefUrl}`);
|
||||||
@@ -15,37 +19,190 @@ mp.events.add('client:initCef', (cefUrl: string, isDebug: boolean) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
browser = mp.browsers.new(cefUrl);
|
browser = mp.browsers.new(cefUrl);
|
||||||
rpc.browser = browser; // Link immediately
|
rpc.browser = browser;
|
||||||
|
|
||||||
mp.gui.chat.push(`[Client] Browser created and linked to RPC`);
|
mp.gui.chat.show(false);
|
||||||
mp.gui.cursor.show(true, true);
|
mp.gui.cursor.show(true, true);
|
||||||
|
cursorVisible = true;
|
||||||
|
|
||||||
|
mp.players.local.freezePosition(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback: auto-link any newly created browser if current is null
|
// Fallback: auto-link browser
|
||||||
mp.events.add('browserCreated', (b: BrowserMp) => {
|
mp.events.add('browserCreated', (b: BrowserMp) => {
|
||||||
if (!rpc.browser) {
|
if (!rpc.browser) {
|
||||||
rpc.browser = b;
|
rpc.browser = b;
|
||||||
mp.gui.chat.push(`[Client] RPC Browser auto-linked via browserCreated`);
|
|
||||||
}
|
}
|
||||||
mp.gui.chat.show(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Custom Chat Handlers via RPC
|
// ─────────────── Keybinds ───────────────────
|
||||||
rpc.register('chat:toggleInput', (state: boolean) => {
|
|
||||||
// This allows you to freeze other inputs while typing if you build a UI manager later
|
// Backtick (`) — toggle cursor
|
||||||
|
mp.keys.bind(0xC0, true, () => {
|
||||||
|
cursorVisible = !cursorVisible;
|
||||||
|
mp.gui.cursor.show(cursorVisible, cursorVisible);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// F2 — toggle admin noclip
|
||||||
|
mp.keys.bind(0x71, true, async () => {
|
||||||
|
if (adminLevel <= 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: any = await rpc.callServer('server:admin:noclip', []);
|
||||||
|
if (!result || !result.allowed) return;
|
||||||
|
|
||||||
|
noclipActive = result.active;
|
||||||
|
|
||||||
|
if (noclipActive) {
|
||||||
|
enableNoclip();
|
||||||
|
} else {
|
||||||
|
disableNoclip();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// silently fail
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────── Noclip Logic ───────────────
|
||||||
|
|
||||||
|
let isNoClip = false;
|
||||||
|
let noClipPos: Vector3Mp | null = null;
|
||||||
|
|
||||||
|
function enableNoclip() {
|
||||||
|
isNoClip = true;
|
||||||
|
noClipPos = mp.players.local.position;
|
||||||
|
|
||||||
|
mp.players.local.freezePosition(true);
|
||||||
|
mp.players.local.setInvincible(true);
|
||||||
|
mp.players.local.setVisible(false, false);
|
||||||
|
mp.players.local.setCollision(false, false);
|
||||||
|
|
||||||
|
mp.gui.cursor.show(false, false);
|
||||||
|
cursorVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableNoclip() {
|
||||||
|
isNoClip = false;
|
||||||
|
|
||||||
|
if (noClipPos) {
|
||||||
|
mp.players.local.setCoordsNoOffset(noClipPos.x, noClipPos.y, noClipPos.z, false, false, false);
|
||||||
|
noClipPos = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.players.local.freezePosition(false);
|
||||||
|
mp.players.local.setInvincible(false);
|
||||||
|
mp.players.local.setVisible(true, false);
|
||||||
|
mp.players.local.setCollision(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Math helpers
|
||||||
|
function getCamDirection() {
|
||||||
|
const rot = mp.game.cam.getGameplayCamRot(2);
|
||||||
|
const z = rot.z * (Math.PI / 180.0);
|
||||||
|
const x = rot.x * (Math.PI / 180.0);
|
||||||
|
const num = Math.abs(Math.cos(x));
|
||||||
|
|
||||||
|
return new mp.Vector3(
|
||||||
|
-Math.sin(z) * num,
|
||||||
|
Math.cos(z) * num,
|
||||||
|
Math.sin(x)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.events.add('render', () => {
|
||||||
|
if (!isNoClip || !noClipPos) return;
|
||||||
|
|
||||||
|
// Fast movement using Shift
|
||||||
|
let speed = 1.0;
|
||||||
|
if (mp.keys.isDown(0x10)) speed = 3.0; // Shift
|
||||||
|
if (mp.keys.isDown(0x11)) speed = 0.2; // Ctrl
|
||||||
|
|
||||||
|
// We do NOT disable controls — we let the user look around natively
|
||||||
|
// But we override position manually.
|
||||||
|
|
||||||
|
const dir = getCamDirection();
|
||||||
|
|
||||||
|
// W = Forward
|
||||||
|
if (mp.keys.isDown(0x57)) {
|
||||||
|
noClipPos.x += dir.x * speed;
|
||||||
|
noClipPos.y += dir.y * speed;
|
||||||
|
noClipPos.z += dir.z * speed;
|
||||||
|
}
|
||||||
|
// S = Backward
|
||||||
|
if (mp.keys.isDown(0x53)) {
|
||||||
|
noClipPos.x -= dir.x * speed;
|
||||||
|
noClipPos.y -= dir.y * speed;
|
||||||
|
noClipPos.z -= dir.z * speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A = Left
|
||||||
|
if (mp.keys.isDown(0x41)) {
|
||||||
|
noClipPos.x += dir.y * speed;
|
||||||
|
noClipPos.y -= dir.x * speed;
|
||||||
|
}
|
||||||
|
// D = Right
|
||||||
|
if (mp.keys.isDown(0x44)) {
|
||||||
|
noClipPos.x -= dir.y * speed;
|
||||||
|
noClipPos.y += dir.x * speed;
|
||||||
|
}
|
||||||
|
// Space = Up
|
||||||
|
if (mp.keys.isDown(0x20)) {
|
||||||
|
noClipPos.z += speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use direct position assignment instead of setCoordsNoOffset
|
||||||
|
// Function calls to natives in a render loop cause massive FPS drops on low-end hardware
|
||||||
|
// because they force physics recalculations. Assignment skips some hooks.
|
||||||
|
mp.players.local.position = noClipPos;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────── Auth Handlers ──────────────
|
||||||
|
|
||||||
|
rpc.register('auth:login', (email: string, password: string) => {
|
||||||
|
return rpc.callServer('server:auth:login', [email, password]);
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.register('auth:register', (email: string, password: string, firstName: string, lastName: string) => {
|
||||||
|
return rpc.callServer('server:auth:register', [email, password, firstName, lastName]);
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.register('client:authSuccess', (rpName: string, level: number) => {
|
||||||
|
adminLevel = level || 0;
|
||||||
|
|
||||||
|
mp.players.local.freezePosition(false);
|
||||||
|
mp.gui.cursor.show(false, false);
|
||||||
|
cursorVisible = false;
|
||||||
|
|
||||||
|
rpc.callBrowser('app:showGame', []).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save credentials to RAGE:MP local storage
|
||||||
|
rpc.register('client:saveCredentials', (email: string, password: string) => {
|
||||||
|
mp.storage.data.auth = { email, password };
|
||||||
|
mp.storage.flush();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load saved credentials from RAGE:MP local storage
|
||||||
|
rpc.register('client:loadCredentials', () => {
|
||||||
|
const auth = mp.storage.data.auth;
|
||||||
|
if (auth && auth.email) {
|
||||||
|
return { email: auth.email, password: auth.password || '' };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─────────────── Chat Handlers ──────────────
|
||||||
|
|
||||||
|
rpc.register('chat:toggleInput', (state: boolean) => {});
|
||||||
|
|
||||||
rpc.register('chat:sendMessage', (msg: string) => {
|
rpc.register('chat:sendMessage', (msg: string) => {
|
||||||
// Send the message natively to the server via our RPC
|
|
||||||
rpc.callServer('server:chat:receive', [msg]);
|
rpc.callServer('server:chat:receive', [msg]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for structured chat broadcasts from the server (Player chat)
|
|
||||||
rpc.register('chat:push:custom', (chatData: any) => {
|
rpc.register('chat:push:custom', (chatData: any) => {
|
||||||
rpc.callBrowser('chat:addMessage', [chatData]);
|
rpc.callBrowser('chat:addMessage', [chatData]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for incoming native string messages from the server (System prints/announcements)
|
|
||||||
mp.events.add('chat:push', (text: string) => {
|
mp.events.add('chat:push', (text: string) => {
|
||||||
rpc.callBrowser('chat:addMessage', [{ type: 'system', text: text }]);
|
rpc.callBrowser('chat:addMessage', [{ type: 'system', text: text }]);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user