feat: 사용자 인증(Authentication)을 위한 로그인(Login) 및 회원가입(Signup) 기능을 구현하고 관련 UI, API, 테스트 코드를 추가했습니다.
This commit is contained in:
parent
ea4eefb525
commit
fee3c1912b
|
|
@ -0,0 +1,33 @@
|
|||
# Personal Authentication Mod Flow
|
||||
|
||||
This document details the modifications and features integrated within `spt-launcher` and `server-csharp` to support the custom Personal Authentication Mod.
|
||||
|
||||
## What is Personal Authentication?
|
||||
Unlike the vanilla SPT launcher which assumes 1 user = 1 machine, Personal Authentication enables multiple isolated accounts. Users register and log in to uniquely generated session IDs via `AuthRouter.cs` hooking into `LauncherCallbacks`.
|
||||
|
||||
## The `server-csharp` Modifications:
|
||||
The server's default JSON payload parsing was too strict regarding unmapped JSON structures (like adding a `password` field).
|
||||
|
||||
We addressed this by updating the base interface in `SPTarkov.Server.Core`:
|
||||
```csharp
|
||||
namespace SPTarkov.Server.Core.Models.Eft.Launcher;
|
||||
|
||||
public record LoginRequestData : IRequestData
|
||||
{
|
||||
[JsonPropertyName("username")]
|
||||
public string? Username { get; set; }
|
||||
|
||||
[JsonPropertyName("password")]
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
```
|
||||
Now, `LoginRequestData` natively accepts passwords, allowing Harmony Patches on `LauncherCallbacks.Login` and `LauncherCallbacks.Register` to validate against the database gracefully.
|
||||
|
||||
## Custom Token Session vs Default Random Session Strings
|
||||
Vanilla SPT creates a random GUID session variable that is used loosely.
|
||||
With SSO:
|
||||
1. `spt-launcher` initiates an HTTP POST `/launcher/profile/login` with `username` and `password`.
|
||||
2. The Database (`DatabaseManager.cs`) verifies credentials against PostgreSQL passwords (hashed).
|
||||
3. Returns a specific `SessionID` mapped directly to that User ID.
|
||||
4. The launcher preserves this ID in cookies and local cache storage.
|
||||
5. Sub-requests (Start Client, Fetch Match Profiles) utilize this single, constant Session ID instead of performing a secondary manual profile scan discovery via `/launcher/profiles`.
|
||||
|
|
@ -125,6 +125,11 @@ const checkServerHealth = async (): Promise<ServerHealthResult> => {
|
|||
const request = net.request({
|
||||
method: "GET",
|
||||
url,
|
||||
headers: {
|
||||
"Accept-Encoding": "identity",
|
||||
requestcompressed: "0",
|
||||
responsecompressed: "0",
|
||||
}
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
|
|
@ -1336,8 +1341,10 @@ const postText = async (path: string, body: unknown) => {
|
|||
|
||||
const requestSessionId = async (
|
||||
username: string,
|
||||
password?: string,
|
||||
): Promise<SessionRequestResult> => {
|
||||
const loginResult = await postText("/launcher/profile/login", { username });
|
||||
// 1. Login to get the session token
|
||||
const loginResult = await postText("/launcher/profile/login", { username, password });
|
||||
if (!loginResult.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
|
|
@ -1347,10 +1354,17 @@ const requestSessionId = async (
|
|||
}
|
||||
|
||||
const sessionId = loginResult.data?.trim();
|
||||
if (!sessionId) {
|
||||
return { ok: false, error: "empty_session", url: loginResult.url };
|
||||
|
||||
// Check if session ID is empty or explicitly "FAILED"
|
||||
if (!sessionId || sessionId === "FAILED" || sessionId === "") {
|
||||
return {
|
||||
ok: false,
|
||||
error: sessionId === "FAILED" ? "login_failed_credentials" : "empty_session",
|
||||
url: loginResult.url
|
||||
};
|
||||
}
|
||||
|
||||
// With PersonalAuthMod, the sessionId is the unique token we need.
|
||||
return { ok: true, sessionId, url: loginResult.url };
|
||||
};
|
||||
|
||||
|
|
@ -1359,6 +1373,7 @@ const isSessionInvalidStatus = (status?: number) =>
|
|||
|
||||
const getSessionIdForUser = async (
|
||||
username: string,
|
||||
password?: string,
|
||||
): Promise<SessionLookupResult> => {
|
||||
const cached = await readSessionRecord();
|
||||
if (cached && cached.username === username && !isSessionExpired(cached)) {
|
||||
|
|
@ -1378,7 +1393,7 @@ const getSessionIdForUser = async (
|
|||
await clearSessionRecord();
|
||||
}
|
||||
|
||||
const loginResult = await requestSessionId(username);
|
||||
const loginResult = await requestSessionId(username, password);
|
||||
if (!loginResult.ok) {
|
||||
return loginResult;
|
||||
}
|
||||
|
|
@ -1392,9 +1407,10 @@ const getSessionIdForUser = async (
|
|||
};
|
||||
};
|
||||
|
||||
const registerProfile = async (username: string) => {
|
||||
return await postJson("/launcher/profile/register", {
|
||||
const registerProfile = async (username: string, password?: string) => {
|
||||
return await postText("/launcher/profile/register", {
|
||||
username,
|
||||
password,
|
||||
edition: "Standard",
|
||||
});
|
||||
};
|
||||
|
|
@ -1420,6 +1436,22 @@ const createWindow = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
console.log("[spt-launcher] Another instance is already running. Quitting...");
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", (event, commandLine, workingDirectory) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
if (windows.length > 0) {
|
||||
const mainWindow = windows[0];
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
app.whenReady().then(() => {
|
||||
const expectedServerUrl = new URL(SERVER_BASE_URL);
|
||||
const expectedServerOrigin = expectedServerUrl.origin;
|
||||
|
|
@ -1687,9 +1719,42 @@ app.whenReady().then(() => {
|
|||
});
|
||||
|
||||
ipcMain.handle(
|
||||
"spt:fetchProfile",
|
||||
async (_event, payload: { username: string }) => {
|
||||
"spt:register",
|
||||
async (_event, payload: { username: string; password?: string }) => {
|
||||
const username = payload.username.trim();
|
||||
const password = payload.password?.trim();
|
||||
return await registerProfile(username, password);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"spt:login",
|
||||
async (_event, payload: { username: string; password?: string }) => {
|
||||
const username = payload.username.trim();
|
||||
const password = payload.password?.trim();
|
||||
|
||||
const sessionResult = await getSessionIdForUser(username, password);
|
||||
if (!sessionResult.ok) {
|
||||
return sessionResult;
|
||||
}
|
||||
|
||||
await writeSessionRecord({ username, sessionId: sessionResult.sessionId });
|
||||
|
||||
return await postJson(
|
||||
"/launcher/profile/info",
|
||||
{ username },
|
||||
{
|
||||
Cookie: `PHPSESSID=${sessionResult.sessionId}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"spt:fetchProfile",
|
||||
async (_event, payload: { username: string; password?: string }) => {
|
||||
const username = payload.username.trim();
|
||||
const password = payload.password?.trim();
|
||||
let activeSessionId: string | undefined;
|
||||
|
||||
const fetchProfileInfo = async (): Promise<{
|
||||
|
|
@ -1699,7 +1764,7 @@ app.whenReady().then(() => {
|
|||
error?: string;
|
||||
url: string;
|
||||
}> => {
|
||||
const sessionResult = await getSessionIdForUser(username);
|
||||
const sessionResult = await getSessionIdForUser(username, password);
|
||||
if (!sessionResult.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
|
|
@ -1720,7 +1785,7 @@ app.whenReady().then(() => {
|
|||
|
||||
let infoResult = await fetchProfileInfo();
|
||||
if (!infoResult.ok && isSessionInvalidStatus(infoResult.status)) {
|
||||
const relogin = await requestSessionId(username);
|
||||
const relogin = await requestSessionId(username, password);
|
||||
if (!relogin.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
|
|
@ -1745,8 +1810,7 @@ app.whenReady().then(() => {
|
|||
return infoResult;
|
||||
}
|
||||
|
||||
await registerProfile(username);
|
||||
return await fetchProfileInfo();
|
||||
return infoResult;
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1904,3 +1968,4 @@ app.on("window-all-closed", () => {
|
|||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ contextBridge.exposeInMainWorld("sptLauncher", {
|
|||
setSptInstallPath: (payload: { path: string; allowMissing?: boolean }) =>
|
||||
ipcRenderer.invoke("spt:setSptInstallPath", payload),
|
||||
pickSptInstallPath: () => ipcRenderer.invoke("spt:pickSptInstallPath"),
|
||||
fetchProfile: (payload: { username: string }) =>
|
||||
login: (payload: { username: string; password?: string }) =>
|
||||
ipcRenderer.invoke("spt:login", payload),
|
||||
register: (payload: { username: string; password?: string }) =>
|
||||
ipcRenderer.invoke("spt:register", payload),
|
||||
fetchProfile: (payload: { username: string; password?: string }) =>
|
||||
ipcRenderer.invoke("spt:fetchProfile", payload),
|
||||
downloadProfile: (payload: { username: string }) =>
|
||||
ipcRenderer.invoke("spt:downloadProfile", payload),
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ const App = () => {
|
|||
const [hasSession, setHasSession] = useState(false);
|
||||
const [loginId, setLoginId] = useState("");
|
||||
const [loginPassword, setLoginPassword] = useState("");
|
||||
const [signupId, setSignupId] = useState("");
|
||||
const [signupPassword, setSignupPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [profileLoading, setProfileLoading] = useState(false);
|
||||
const [profile, setProfile] = useState<{
|
||||
username: string;
|
||||
|
|
@ -496,11 +499,20 @@ const App = () => {
|
|||
|
||||
sessionResumeAttemptedRef.current = true;
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await window.sptLauncher.resumeSession();
|
||||
if (!result.ok || !result.data) {
|
||||
scheduleScreenTransition("login");
|
||||
return;
|
||||
}
|
||||
|
||||
const resumedProfile = extractProfile(result.data, loginId.trim());
|
||||
// Simple validation: profile must have an ID or Username to be considered valid
|
||||
if (!resumedProfile.id && !resumedProfile.username) {
|
||||
scheduleScreenTransition("login");
|
||||
return;
|
||||
}
|
||||
|
||||
setProfile(resumedProfile);
|
||||
setHasSession(true);
|
||||
if (resumedProfile.username) {
|
||||
|
|
@ -508,6 +520,10 @@ const App = () => {
|
|||
}
|
||||
clearTransitionTimeout();
|
||||
setScreen("main");
|
||||
} catch (error) {
|
||||
console.error("Session resume failed", error);
|
||||
scheduleScreenTransition("login");
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
screen,
|
||||
|
|
@ -815,28 +831,30 @@ const App = () => {
|
|||
|
||||
const handleLogin = async () => {
|
||||
const trimmedId = loginId.trim();
|
||||
const trimmedPassword = loginPassword.trim();
|
||||
if (!trimmedId) {
|
||||
window.alert("아이디를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.sptLauncher?.fetchProfile) {
|
||||
window.alert("프로필 조회 기능이 준비되지 않았습니다.");
|
||||
if (!window.sptLauncher?.login) {
|
||||
window.alert("로그인 기능이 준비되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setProfileLoading(true);
|
||||
|
||||
try {
|
||||
const result = await window.sptLauncher.fetchProfile({
|
||||
username: trimmedId
|
||||
const result = await window.sptLauncher.login({
|
||||
username: trimmedId,
|
||||
password: trimmedPassword
|
||||
});
|
||||
if (!result.ok) {
|
||||
if (isTimeoutError(result.error)) {
|
||||
enterRecoveryMode("timeout");
|
||||
return;
|
||||
}
|
||||
window.alert(result.error ?? "프로필 조회에 실패했습니다.");
|
||||
window.alert(result.error ?? "로그인에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -860,9 +878,51 @@ const App = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleSignup = () => {
|
||||
window.alert("회원가입 요청이 접수되었습니다. 관리자 승인 후 이용 가능합니다.");
|
||||
const handleSignup = async () => {
|
||||
const trimmedId = signupId.trim();
|
||||
const trimmedPassword = signupPassword.trim();
|
||||
const trimmedConfirm = confirmPassword.trim();
|
||||
|
||||
if (!trimmedId) {
|
||||
window.alert("아이디를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
if (!trimmedPassword) {
|
||||
window.alert("비밀번호를 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
if (trimmedPassword !== trimmedConfirm) {
|
||||
window.alert("비밀번호가 일치하지 않습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.sptLauncher?.register) {
|
||||
window.alert("회원가입 기능이 준비되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
setProfileLoading(true);
|
||||
try {
|
||||
const result = await window.sptLauncher.register({
|
||||
username: trimmedId,
|
||||
password: trimmedPassword
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
window.alert(result.error ?? "회원가입 요청에 실패했습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.alert("회원가입이 완료되었습니다. 이제 로그인할 수 있습니다.");
|
||||
setLoginId(trimmedId);
|
||||
setLoginPassword(trimmedPassword);
|
||||
setScreen("login");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
window.alert(message);
|
||||
} finally {
|
||||
setProfileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordReset = () => {
|
||||
|
|
@ -1043,7 +1103,7 @@ const App = () => {
|
|||
name: s.name,
|
||||
ok: s.result?.ok ?? false,
|
||||
message: s.result?.ok ? undefined : s.result?.message ?? s.result?.error,
|
||||
status: s.result?.ok ? "done" : "error"
|
||||
status: (s.result?.ok ? "done" : "error") as "done" | "error"
|
||||
};
|
||||
upsertSyncStep(uiStep);
|
||||
})
|
||||
|
|
@ -1312,12 +1372,27 @@ const App = () => {
|
|||
회원가입 후 서버 관리자 승인이 완료되어야 로그인 가능합니다.
|
||||
</p>
|
||||
<div className="login-form">
|
||||
<input type="text" placeholder="아이디" />
|
||||
<input type="password" placeholder="비밀번호" />
|
||||
<input type="password" placeholder="비밀번호 확인" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="아이디"
|
||||
value={signupId}
|
||||
onChange={(e) => setSignupId(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="비밀번호"
|
||||
value={signupPassword}
|
||||
onChange={(e) => setSignupPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="비밀번호 확인"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button type="button" onClick={handleSignup}>
|
||||
회원가입 요청
|
||||
<button type="button" onClick={handleSignup} disabled={profileLoading}>
|
||||
{profileLoading ? "가입 요청 중..." : "회원가입 요청"}
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={() => setScreen("login")}>
|
||||
로그인으로 돌아가기
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,10 @@
|
|||
--- src/renderer/App.tsx
|
||||
+++ src/renderer/App.tsx
|
||||
@@ -49,6 +49,7 @@
|
||||
// MOCK For Browser testing since we hit CORS issues
|
||||
if (window.location.protocol === 'http:') {
|
||||
setServerStatus('ready');
|
||||
+ return;
|
||||
}
|
||||
|
||||
const result = await window.sptLauncher.checkServerVersion();
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
@@ -49,6 +49,7 @@
|
||||
// MOCK For Browser testing since we hit CORS issues
|
||||
if (window.location.protocol === 'http:') {
|
||||
setServerStatus('ready');
|
||||
+ return;
|
||||
}
|
||||
|
||||
const result = await window.sptLauncher.checkServerVersion();
|
||||
|
|
@ -121,7 +121,21 @@ interface Window {
|
|||
path?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
fetchProfile: (payload: { username: string }) => Promise<{
|
||||
login: (payload: { username: string; password?: string }) => Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
url: string;
|
||||
}>;
|
||||
register: (payload: { username: string; password?: string }) => Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
url: string;
|
||||
}>;
|
||||
fetchProfile: (payload: { username: string; password?: string }) => Promise<{
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const APP_CONFIG = {
|
||||
serverBaseUrl: "https://pandoli365.com:5069",
|
||||
serverBaseUrl: "https://127.0.0.1:6969",
|
||||
serverHealthcheckPath: "/launcher/ping",
|
||||
serverHealthcheckTimeoutMs: 2000,
|
||||
serverRequestTimeoutMs: 4000,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
const https = require('https');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
|
||||
const makeRequest = (path, data, cookie) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(data);
|
||||
const deflated = zlib.deflateSync(payload);
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Encoding': 'deflate',
|
||||
'Expect': '100-continue'
|
||||
};
|
||||
if (cookie) {
|
||||
headers['Cookie'] = cookie;
|
||||
}
|
||||
|
||||
const req = https.request({
|
||||
hostname: '127.0.0.1',
|
||||
port: 6969,
|
||||
path: path,
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
agent
|
||||
}, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', chunk => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
let body = '';
|
||||
if (buffer.length > 0) {
|
||||
try {
|
||||
body = zlib.inflateSync(buffer).toString();
|
||||
} catch (e) {
|
||||
body = buffer.toString();
|
||||
}
|
||||
}
|
||||
resolve({ status: res.statusCode, body });
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(deflated);
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
async function testFlow() {
|
||||
const user = 'testuser' + Math.floor(Math.random() * 1000);
|
||||
const pass = 'password123';
|
||||
|
||||
console.log(`Registering new user: ${user}`);
|
||||
const regRes = await makeRequest('/launcher/profile/register', {
|
||||
username: user,
|
||||
password: pass,
|
||||
edition: 'Standard'
|
||||
});
|
||||
console.log("Registration Response:", regRes.status, regRes.body);
|
||||
|
||||
if (regRes.status !== 200 || regRes.body === 'FAILED') {
|
||||
console.log("Registration failed, stopping.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\nLogging in with new user...");
|
||||
const loginRes = await makeRequest('/launcher/profile/login', {
|
||||
username: user,
|
||||
password: pass
|
||||
});
|
||||
console.log("Login Response:", loginRes.status, loginRes.body);
|
||||
|
||||
if (loginRes.status !== 200 || loginRes.body === 'FAILED') {
|
||||
console.log("Login failed, stopping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = loginRes.body;
|
||||
|
||||
console.log("\nFetching profile info...");
|
||||
const infoRes = await makeRequest('/launcher/profile/info', {
|
||||
username: user
|
||||
}, `PHPSESSID=${sessionId}`);
|
||||
console.log("Info Response:", infoRes.status, infoRes.body);
|
||||
}
|
||||
|
||||
testFlow().catch(console.error);
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
const http = require('http');
|
||||
const https = require('https');
|
||||
const zlib = require('zlib');
|
||||
|
||||
async function sendRequest(path, data) {
|
||||
const payload = JSON.stringify(data);
|
||||
|
||||
const options = {
|
||||
hostname: '127.0.0.1',
|
||||
port: 6969,
|
||||
path: path,
|
||||
method: 'POST',
|
||||
rejectUnauthorized: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
'Accept-Encoding': 'identity',
|
||||
'requestcompressed': '0',
|
||||
'responsecompressed': '0'
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(options, (res) => {
|
||||
let result = '';
|
||||
res.on('data', d => result += d);
|
||||
res.on('end', () => resolve({ status: res.statusCode, body: result }));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(payload); // We pass uncompressed for now using requestcompressed: 0
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log("Registering...");
|
||||
const regRes = await sendRequest('/launcher/profile/register', { username: 'testuser3', password: 'testpassword3', edition: 'Standard' });
|
||||
console.log(regRes);
|
||||
|
||||
console.log("Logging in...");
|
||||
const loginRes = await sendRequest('/launcher/profile/login', { username: 'testuser3', password: 'testpassword3' });
|
||||
console.log(loginRes);
|
||||
}
|
||||
|
||||
run();
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
const https = require('https');
|
||||
|
||||
const agent = new https.Agent({ rejectUnauthorized: false });
|
||||
|
||||
const makeRequest = (path, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify(data);
|
||||
const req = https.request({
|
||||
hostname: '127.0.0.1',
|
||||
port: 6969,
|
||||
path: path,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload)
|
||||
},
|
||||
agent
|
||||
}, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => resolve({ status: res.statusCode, body }));
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
async function testFlow() {
|
||||
console.log("Registering new user...");
|
||||
const regRes = await makeRequest('/launcher/profile/register', {
|
||||
username: 'newuser123',
|
||||
password: 'newpass123',
|
||||
edition: 'Standard'
|
||||
});
|
||||
console.log("Registration:", regRes);
|
||||
|
||||
console.log("Logging in with new user...");
|
||||
const loginRes = await makeRequest('/launcher/profile/login', {
|
||||
username: 'newuser123',
|
||||
password: 'newpass123'
|
||||
});
|
||||
console.log("Login:", loginRes);
|
||||
const sessionId = loginRes.body;
|
||||
|
||||
console.log("Fetching profile info...");
|
||||
const infoRes = await makeRequest('/launcher/profile/info', {
|
||||
username: 'newuser123'
|
||||
});
|
||||
console.log("Info:", infoRes);
|
||||
}
|
||||
|
||||
testFlow().catch(console.error);
|
||||
Loading…
Reference in New Issue