1 Commits

Author SHA1 Message Date
d79c532009 feat: Enhance login functionality with Discord integration and improve code formatting
Some checks failed
SteamWarCI Build failed
2025-11-13 14:32:06 +01:00
5 changed files with 268 additions and 334 deletions

View File

@@ -18,12 +18,14 @@
--> -->
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy'; import { preventDefault } from "svelte/legacy";
import {l} from "@utils/util.ts"; import { l } from "@utils/util.ts";
import {t} from "astro-i18n"; import { t } from "astro-i18n";
import {get} from "svelte/store"; import { get } from "svelte/store";
import {navigate} from "astro:transitions/client"; import { navigate } from "astro:transitions/client";
import { onMount } from "svelte";
import { authV2Repo } from "./repo/authv2.ts";
let username: string = $state(""); let username: string = $state("");
let pw: string = $state(""); let pw: string = $state("");
@@ -31,7 +33,7 @@
let error: string = $state(""); let error: string = $state("");
async function login() { async function login() {
let {authV2Repo} = await import("./repo/authv2.ts"); let { authV2Repo } = await import("./repo/authv2.ts");
if (username === "" || pw === "") { if (username === "" || pw === "") {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
@@ -52,6 +54,24 @@
error = t("login.error"); error = t("login.error");
} }
} }
onMount(() => {
if (window.location.hash.includes("access_token")) {
const params = new URLSearchParams(window.location.hash.substring(1));
const accessToken = params.get("access_token");
if (accessToken) {
let auth = $authV2Repo.loginDiscord(accessToken);
if (!auth) {
pw = "";
error = t("login.error");
return;
}
navigate(l("/dashboard"));
}
}
});
</script> </script>
<form class="bg-gray-100 dark:bg-neutral-900 p-12 rounded-2xl shadow-2xl border-2 border-gray-600 flex flex-col" onsubmit={preventDefault(login)}> <form class="bg-gray-100 dark:bg-neutral-900 p-12 rounded-2xl shadow-2xl border-2 border-gray-600 flex flex-col" onsubmit={preventDefault(login)}>
@@ -63,12 +83,19 @@
<input type="password" id="password" name="password" placeholder={t("login.placeholder.password")} bind:value={pw} /> <input type="password" id="password" name="password" placeholder={t("login.placeholder.password")} bind:value={pw} />
</div> </div>
<p class="mt-2"> <p class="mt-2">
<a class="text-neutral-500 hover:underline" href={l("/set-password")}>{t("login.setPassword")}</a></p> <a class="text-neutral-500 hover:underline" href={l("/set-password")}>{t("login.setPassword")}</a>
</p>
{#if error} {#if error}
<p class="mt-2 text-red-500">{error}</p> <p class="mt-2 text-red-500">{error}</p>
{/if} {/if}
<button class="btn mt-4 !mx-0 justify-center" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button> <button class="btn mt-4 !mx-0 justify-center" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button>
<a
class="btn mt-4 !mx-0 justify-center"
href="https://discord.com/oauth2/authorize?client_id=869611389818400779&response_type=token&redirect_uri=http%3A%2F%2Flocalhost%3A4321%2Flogin&scope=identify"
>
{t("login.discord")}
</a>
</form> </form>
<style lang="postcss"> <style lang="postcss">
@@ -79,4 +106,4 @@
label { label {
@apply text-neutral-300; @apply text-neutral-300;
} }
</style> </style>

View File

@@ -17,49 +17,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {readable, writable} from "svelte/store"; import { readable, writable } from "svelte/store";
import dayjs, {type Dayjs} from "dayjs"; import { ResponseUserSchema } from "@components/types/data";
import {type AuthToken, AuthTokenSchema} from "@type/auth.ts";
export class AuthV2Repo { export class AuthV2Repo {
private accessToken: string | undefined;
private accessTokenExpires: Dayjs | undefined;
private refreshToken: string | undefined;
private refreshTokenExpires: Dayjs | undefined;
constructor() { constructor() {
if (typeof localStorage === "undefined") { this.request("/data/me").then((value) => {
return; if (value.ok) {
} loggedIn.set(true);
}
this.accessToken = localStorage.getItem("sw-access-token") ?? undefined; });
if (this.accessToken) {
this.accessTokenExpires = dayjs(localStorage.getItem("sw-access-token-expires") ?? "");
}
this.refreshToken = localStorage.getItem("sw-refresh-token") ?? undefined;
if (this.refreshToken) {
loggedIn.set(true);
this.refreshTokenExpires = dayjs(localStorage.getItem("sw-refresh-token-expires") ?? "");
}
} }
async login(name: string, password: string) { async login(name: string, password: string) {
if (this.accessToken !== undefined || this.refreshToken !== undefined) {
throw new Error("Already logged in");
}
try { try {
const login = await this.request("/auth", { await this.request("/auth", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
name, name,
password, password,
keepLoggedIn: true, keepLoggedIn: true,
}), }),
}).then(value => value.json()).then(value => AuthTokenSchema.parse(value)); })
.then((value) => value.json())
.then((value) => ResponseUserSchema.parse(value));
this.setLoginState(login); loggedIn.set(true);
return true; return true;
} catch (e) { } catch (e) {
@@ -67,118 +50,40 @@ export class AuthV2Repo {
} }
} }
async logout() { async loginDiscord(token: string) {
if (this.accessToken === undefined) { try {
return; await this.request("/auth/discord", {
method: "POST",
body: token,
})
.then((value) => value.json())
.then((value) => ResponseUserSchema.parse(value));
loggedIn.set(true);
return true;
} catch (e) {
return false;
} }
}
async logout() {
await this.request("/auth", { await this.request("/auth", {
method: "DELETE", method: "DELETE",
}); });
this.resetAccessToken();
this.resetRefreshToken();
}
private setLoginState(tokens: AuthToken) {
this.setAccessToken(tokens.accessToken.token, dayjs(tokens.accessToken.expires));
this.setRefreshToken(tokens.refreshToken.token, dayjs(tokens.refreshToken.expires));
loggedIn.set(true);
}
private setAccessToken(token: string, expires: Dayjs) {
this.accessToken = token;
this.accessTokenExpires = expires;
localStorage.setItem("sw-access-token", token);
localStorage.setItem("sw-access-token-expires", expires.toString());
}
private resetAccessToken() {
if (this.accessToken === undefined) {
return;
}
this.accessToken = undefined;
this.accessTokenExpires = undefined;
localStorage.removeItem("sw-access-token");
localStorage.removeItem("sw-access-token-expires");
}
private setRefreshToken(token: string, expires: Dayjs) {
this.refreshToken = token;
this.refreshTokenExpires = expires;
localStorage.setItem("sw-refresh-token", token);
localStorage.setItem("sw-refresh-token-expires", expires.toString());
}
private resetRefreshToken() {
if (this.refreshToken === undefined) {
return;
}
this.refreshToken = undefined;
this.refreshTokenExpires = undefined;
localStorage.removeItem("sw-refresh-token");
localStorage.removeItem("sw-refresh-token-expires");
loggedIn.set(false); loggedIn.set(false);
} }
private async refresh() { async request(url: string, params: RequestInit = {}) {
if (this.refreshToken === undefined || this.refreshTokenExpires === undefined || this.refreshTokenExpires.isBefore(dayjs().add(10, "seconds"))) { return fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {
this.resetRefreshToken(); ...params,
this.resetAccessToken(); credentials: "include",
return;
}
const response = await this.requestWithToken(this.refreshToken!, "/auth", {
method: "PUT",
}).then(value => {
if (value.status === 401) {
this.resetRefreshToken();
this.resetAccessToken();
return undefined;
}
return value.json();
}).then(value => AuthTokenSchema.parse(value));
this.setLoginState(response);
}
async request(url: string, params: RequestInit = {}, retryCount: number = 0) {
if (this.accessToken !== undefined && this.accessTokenExpires !== undefined && this.accessTokenExpires.isBefore(dayjs().add(10, "seconds"))) {
await this.refresh();
}
return this.requestWithToken(this.accessToken ?? "", url, params, retryCount);
}
private async requestWithToken(token: string, url: string, params: RequestInit = {}, retryCount: number = 0): Promise<Response> {
if (retryCount >= 3) {
throw new Error("Too many retries");
}
return fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {...params,
headers: { headers: {
...(token !== "" ? {"Authorization": "Bearer " + (token)} : {}), "Content-Type": "application/json",
"Content-Type": "application/json", ...params.headers, ...params.headers,
}, },
}) });
.then(async value => {
if (value.status === 401 && url !== "/auth") {
try {
await this.refresh();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_e) { /* empty */ }
return this.request(url, params, retryCount + 1);
}
return value;
});
} }
} }
export const loggedIn = writable(false); export const loggedIn = writable(false);
export const authV2Repo = readable(new AuthV2Repo()); export const authV2Repo = readable(new AuthV2Repo());

View File

@@ -223,6 +223,7 @@
}, },
"setPassword": "Wie setze ich mein Passwort?", "setPassword": "Wie setze ich mein Passwort?",
"submit": "Login", "submit": "Login",
"discord": "Mit Discord Einloggen",
"error": "Falscher Nutzername oder falsches Passwort" "error": "Falscher Nutzername oder falsches Passwort"
}, },
"ranked": { "ranked": {

View File

@@ -1,196 +1,197 @@
{ {
"navbar": { "navbar": {
"links": { "links": {
"home": { "home": {
"title": "Home", "title": "Home",
"announcements": "Announcements",
"about": "About",
"downloads": "Downloads",
"faq": "FAQ"
},
"rules": {
"title": "Game modes",
"rotating": "Rotating",
"general": "General",
"coc": "Code of Conduct"
},
"help": {
"title": "Help",
"center": "Helpcenter",
"docs": "Docs"
},
"account": "Account"
}
},
"status": {
"loading": "Loading...",
"players": "Players: {# count #}"
},
"home": {
"page": "SteamWar - Home",
"subtitle": {
"1": "WarGears, AirShips, WarShips",
"2": "Players Online: "
},
"join": "Join Now",
"benefits": {
"fights": {
"title": "Exciting Fights",
"description": {
"1": "Compete with other players in the arena and show that you are the best.",
"2": "From small combat machines to huge battleships, everything can be built."
}
},
"bau": {
"title": "Own Build Server",
"description": "Every player gets their own build server to ensure maximum performance and minimal limitations with leading tools like FaWe or Axiom"
},
"minigames": {
"title": "Minigames",
"description": {
"1": "Besides the Arena, you can also play minigames with other players.",
"2": "like MissleWars, Towerrun or TNTLeague"
}
},
"open": {
"description": "Playing on SteamWar is completely free and our software is open source."
},
"read": "Read More"
},
"prefix": {
"Admin": "Admin",
"Dev": "Developer",
"Mod": "Moderator",
"Sup": "Supporter",
"Arch": "Builder",
"User": "User",
"YT": "YouTuber"
}
},
"footer": {
"imprint": "Imprint",
"privacy": "Privacy Policy",
"coc": "Code of Conduct",
"stats": "Stats",
"gamemodes": "Game modes",
"announcements": "Announcements", "announcements": "Announcements",
"about": "About", "join": "Join Now"
"downloads": "Downloads",
"faq": "FAQ"
},
"rules": {
"title": "Game modes",
"rotating": "Rotating",
"general": "General",
"coc": "Code of Conduct"
},
"help": {
"title": "Help",
"center": "Helpcenter",
"docs": "Docs"
},
"account": "Account"
}
},
"status": {
"loading": "Loading...",
"players": "Players: {# count #}"
},
"home": {
"page": "SteamWar - Home",
"subtitle": {
"1": "WarGears, AirShips, WarShips",
"2": "Players Online: "
}, },
"join": "Join Now", "ranking": {
"benefits": { "heading": "{# mode #} Rankings"
"fights": {
"title": "Exciting Fights",
"description": {
"1": "Compete with other players in the arena and show that you are the best.",
"2": "From small combat machines to huge battleships, everything can be built."
}
},
"bau": {
"title": "Own Build Server",
"description": "Every player gets their own build server to ensure maximum performance and minimal limitations with leading tools like FaWe or Axiom"
},
"minigames": {
"title": "Minigames",
"description": {
"1": "Besides the Arena, you can also play minigames with other players.",
"2": "like MissleWars, Towerrun or TNTLeague"
}
},
"open": {
"description": "Playing on SteamWar is completely free and our software is open source."
},
"read": "Read More"
}, },
"prefix": { "announcements": {
"Admin": "Admin", "table": {
"Dev": "Developer", "time": "Time",
"Mod": "Moderator", "blue": "Blue Team",
"Sup": "Supporter", "red": "Red Team",
"Arch": "Builder", "winner": "Winner",
"User": "User", "notPlayed": "Not Played",
"YT": "YouTuber" "draw": "Draw",
} "team": "Team",
}, "points": "Points"
"footer": { }
"imprint": "Imprint", },
"privacy": "Privacy Policy", "elo": {
"coc": "Code of Conduct", "place": "Place"
"stats": "Stats", },
"gamemodes": "Game modes", "warning": {
"announcements": "Announcements", "title": "This page is not available in your language.",
"join": "Join Now" "text": "The page you are trying to access is not available in your language. You can still access the original page in German."
}, },
"ranking": { "blog": {
"heading": "{# mode #} Rankings" "title": "Announcements - SteamWar"
}, },
"announcements": { "dashboard": {
"table": { "title": "Hello, {# name #}!",
"time": "Time", "rank": "Rank: {# rank #}",
"blue": "Blue Team", "permissions": "Permssions:",
"red": "Red Team", "buttons": {
"winner": "Winner", "logout": "Logout"
"notPlayed": "Not Played", },
"draw": "Draw", "stats": {
"team": "Team", "playtime": "Playtime: {# playtime #}",
"points": "Points" "fights": "Fights: {# fights #}",
} "checked": "Accepted Schematics: {# checked #}"
}, },
"elo": { "schematic": {
"place": "Place" "upload": "Upload",
}, "cancel": "Cancel",
"warning": { "title": "Upload Schematic",
"title": "This page is not available in your language.", "dir": "Directory",
"text": "The page you are trying to access is not available in your language. You can still access the original page in German." "head": {
}, "type": "Type",
"blog": { "owner": "Owner",
"title": "Announcements - SteamWar" "updated": "Updated",
}, "replaceColor": "Replace Color",
"dashboard": { "allowReplay": "Allow Replay"
"title": "Hello, {# name #}!", },
"rank": "Rank: {# rank #}", "info": {
"permissions": "Permssions:", "path": "Path: {# path #}",
"buttons": { "replaceColor": "Replace Color: ",
"logout": "Logout" "allowReplay": "Allow Replay: ",
"type": "Type: {# type #}",
"updated": "Updated: {# updated #}",
"item": "Item: {# item #}",
"members": "Members: {# members #}",
"btn": {
"download": "Download",
"close": "Close"
}
},
"errors": {
"invalidEnding": "This file extension cannot be uploaded.",
"noFile": "No file.",
"upload": "Error uploading, check your schematic!"
}
}
},
"login": {
"page": "SteamWar - Login",
"title": "Login",
"placeholder": {
"username": "Username...",
"password": "***************"
},
"label": {
"username": "Username",
"password": "Password",
"repeat": "Repeat Password"
},
"setPassword": "How to set a Password",
"submit": "Login",
"discord": "Login with Discord",
"error": "Invalid username or password"
},
"ranked": {
"title": "{# mode #} - Ranking"
},
"rules": {
"page": "SteamWar - Rules",
"wg": {
"description": "Today, the battlefields of Earth are dominated by heavy artillery. With our traditional rules, WarGears are also arena-wrecking heavyweights. Due to the cannon technology with the most projectiles, you can expect hard and short-lived battles in WarGears."
},
"as": {
"description": "The dream of flying has inspired humanity for millennia. The AirShips game mode offers you the almost unlimited possibilities of the sky. Whether you compete with 15 2-projectile cannons or 2 15-projectile cannons, you always have a realistic chance of winning. Because: Everything has its price."
},
"ws": {
"description": "For a long time, warships were the ultimate weapon of war. This is still true for Warships today in terms of rocket and slime technology. Due to the limited cannon power, WarShips offer long, intense and varied battles, with new techniques always being introduced in the arena. After a while, a WarShip battle shifts to the water through boarding, providing exciting PvP action."
},
"mwg": {
"description": "In today's urban warfare, there is no place for heavy equipment, which is why smaller machines still have their place today. With their slightly smaller cannons, MiniWarGears are the perfect choice for beginners, casual players, and those who like to experiment."
},
"qg": {
"description": "Sometimes there is no time for a long construction. Sometimes it has to be quick. For these cases there are QuickGears. Without quality control and with just one click you can create a vehicle here. The quality is not always the best, but for a quick fight it is enough."
},
"rules": "Rules »",
"announcements": "Announcements »",
"ranking": "Ranking »",
"title": "{# mode #} - Rules"
}, },
"stats": { "stats": {
"playtime": "Playtime: {# playtime #}", "title": "Fight Statistics"
"fights": "Fights: {# fights #}",
"checked": "Accepted Schematics: {# checked #}"
}, },
"schematic": { "404": {
"upload": "Upload", "title": "404 - Page not found",
"cancel": "Cancel", "description": "Page not found"
"title": "Upload Schematic",
"dir": "Directory",
"head": {
"type": "Type",
"owner": "Owner",
"updated": "Updated",
"replaceColor": "Replace Color",
"allowReplay": "Allow Replay"
},
"info": {
"path": "Path: {# path #}",
"replaceColor": "Replace Color: ",
"allowReplay": "Allow Replay: ",
"type": "Type: {# type #}",
"updated": "Updated: {# updated #}",
"item": "Item: {# item #}",
"members": "Members: {# members #}",
"btn": {
"download": "Download",
"close": "Close"
}
},
"errors": {
"invalidEnding": "This file extension cannot be uploaded.",
"noFile": "No file.",
"upload": "Error uploading, check your schematic!"
}
} }
},
"login": {
"page": "SteamWar - Login",
"title": "Login",
"placeholder": {
"username": "Username...",
"password": "***************"
},
"label": {
"username": "Username",
"password": "Password",
"repeat": "Repeat Password"
},
"setPassword": "How to set a Password",
"submit": "Login",
"error": "Invalid username or password"
},
"ranked": {
"title": "{# mode #} - Ranking"
},
"rules": {
"page": "SteamWar - Rules",
"wg": {
"description": "Today, the battlefields of Earth are dominated by heavy artillery. With our traditional rules, WarGears are also arena-wrecking heavyweights. Due to the cannon technology with the most projectiles, you can expect hard and short-lived battles in WarGears."
},
"as": {
"description": "The dream of flying has inspired humanity for millennia. The AirShips game mode offers you the almost unlimited possibilities of the sky. Whether you compete with 15 2-projectile cannons or 2 15-projectile cannons, you always have a realistic chance of winning. Because: Everything has its price."
},
"ws": {
"description": "For a long time, warships were the ultimate weapon of war. This is still true for Warships today in terms of rocket and slime technology. Due to the limited cannon power, WarShips offer long, intense and varied battles, with new techniques always being introduced in the arena. After a while, a WarShip battle shifts to the water through boarding, providing exciting PvP action."
},
"mwg": {
"description": "In today's urban warfare, there is no place for heavy equipment, which is why smaller machines still have their place today. With their slightly smaller cannons, MiniWarGears are the perfect choice for beginners, casual players, and those who like to experiment."
},
"qg": {
"description": "Sometimes there is no time for a long construction. Sometimes it has to be quick. For these cases there are QuickGears. Without quality control and with just one click you can create a vehicle here. The quality is not always the best, but for a quick fight it is enough."
},
"rules": "Rules »",
"announcements": "Announcements »",
"ranking": "Ranking »",
"title": "{# mode #} - Rules"
},
"stats": {
"title": "Fight Statistics"
},
"404": {
"title": "404 - Page not found",
"description": "Page not found"
}
} }

View File

@@ -1,21 +1,21 @@
--- ---
import LoginComponent from "@components/Login.svelte"; import LoginComponent from "@components/Login.svelte";
import NavbarLayout from "@layouts/NavbarLayout.astro"; import NavbarLayout from "@layouts/NavbarLayout.astro";
import {t} from "astro-i18n"; import { t } from "astro-i18n";
import BackgroundImage from "../components/BackgroundImage.astro"; import BackgroundImage from "../components/BackgroundImage.astro";
--- ---
<NavbarLayout title={t("login.page")}> <NavbarLayout title={t("login.page")}>
<script> <script>
import {l} from "../util/util"; import { l } from "../util/util";
import {navigate} from "astro:transitions/client"; import { navigate } from "astro:transitions/client";
import {loggedIn} from "../components/repo/authv2"; import { loggedIn } from "../components/repo/authv2";
import {get} from "svelte/store"; import { get } from "svelte/store";
document.addEventListener("astro:page-load", () => { document.addEventListener("astro:page-load", () => {
if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) { if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) {
if (get(loggedIn)) { if (get(loggedIn)) {
navigate(l("/dashboard"), {history: "replace"}); navigate(l("/dashboard"), { history: "replace" });
} }
} }
}); });
@@ -23,8 +23,8 @@ import BackgroundImage from "../components/BackgroundImage.astro";
<div class="h-screen w-screen fixed -z-10"> <div class="h-screen w-screen fixed -z-10">
<BackgroundImage /> <BackgroundImage />
</div> </div>
<div class="h-screen mx-auto p-8 rounded-b-md pt-40 sm:pt-28 md:pt-14 flex flex-col justify-center items-center <div class="h-screen mx-auto p-8 rounded-b-md pt-40 sm:pt-28 md:pt-14 flex flex-col justify-center items-center
dark:text-white " style="width: min(100vw, 75em);"> dark:text-white" style="width: min(100vw, 75em);">
<LoginComponent client:load/> <LoginComponent client:load />
</div> </div>
</NavbarLayout> </NavbarLayout>