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">
import { preventDefault } from 'svelte/legacy';
import { preventDefault } from "svelte/legacy";
import {l} from "@utils/util.ts";
import {t} from "astro-i18n";
import {get} from "svelte/store";
import {navigate} from "astro:transitions/client";
import { l } from "@utils/util.ts";
import { t } from "astro-i18n";
import { get } from "svelte/store";
import { navigate } from "astro:transitions/client";
import { onMount } from "svelte";
import { authV2Repo } from "./repo/authv2.ts";
let username: string = $state("");
let pw: string = $state("");
@@ -31,7 +33,7 @@
let error: string = $state("");
async function login() {
let {authV2Repo} = await import("./repo/authv2.ts");
let { authV2Repo } = await import("./repo/authv2.ts");
if (username === "" || pw === "") {
pw = "";
error = t("login.error");
@@ -52,6 +54,24 @@
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>
<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} />
</div>
<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}
<p class="mt-2 text-red-500">{error}</p>
{/if}
<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>
<style lang="postcss">
@@ -79,4 +106,4 @@
label {
@apply text-neutral-300;
}
</style>
</style>

View File

@@ -17,49 +17,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {readable, writable} from "svelte/store";
import dayjs, {type Dayjs} from "dayjs";
import {type AuthToken, AuthTokenSchema} from "@type/auth.ts";
import { readable, writable } from "svelte/store";
import { ResponseUserSchema } from "@components/types/data";
export class AuthV2Repo {
private accessToken: string | undefined;
private accessTokenExpires: Dayjs | undefined;
private refreshToken: string | undefined;
private refreshTokenExpires: Dayjs | undefined;
constructor() {
if (typeof localStorage === "undefined") {
return;
}
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") ?? "");
}
this.request("/data/me").then((value) => {
if (value.ok) {
loggedIn.set(true);
}
});
}
async login(name: string, password: string) {
if (this.accessToken !== undefined || this.refreshToken !== undefined) {
throw new Error("Already logged in");
}
try {
const login = await this.request("/auth", {
await this.request("/auth", {
method: "POST",
body: JSON.stringify({
name,
password,
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;
} catch (e) {
@@ -67,118 +50,40 @@ export class AuthV2Repo {
}
}
async logout() {
if (this.accessToken === undefined) {
return;
async loginDiscord(token: string) {
try {
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", {
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);
}
private async refresh() {
if (this.refreshToken === undefined || this.refreshTokenExpires === undefined || this.refreshTokenExpires.isBefore(dayjs().add(10, "seconds"))) {
this.resetRefreshToken();
this.resetAccessToken();
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,
async request(url: string, params: RequestInit = {}) {
return fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {
...params,
credentials: "include",
headers: {
...(token !== "" ? {"Authorization": "Bearer " + (token)} : {}),
"Content-Type": "application/json", ...params.headers,
"Content-Type": "application/json",
...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 authV2Repo = readable(new AuthV2Repo());
export const authV2Repo = readable(new AuthV2Repo());

View File

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

View File

@@ -1,196 +1,197 @@
{
"navbar": {
"links": {
"home": {
"title": "Home",
"navbar": {
"links": {
"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",
"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"
},
"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"
"ranking": {
"heading": "{# mode #} Rankings"
},
"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",
"join": "Join Now"
},
"ranking": {
"heading": "{# mode #} Rankings"
},
"announcements": {
"table": {
"time": "Time",
"blue": "Blue Team",
"red": "Red Team",
"winner": "Winner",
"notPlayed": "Not Played",
"draw": "Draw",
"team": "Team",
"points": "Points"
}
},
"elo": {
"place": "Place"
},
"warning": {
"title": "This page is not available in your language.",
"text": "The page you are trying to access is not available in your language. You can still access the original page in German."
},
"blog": {
"title": "Announcements - SteamWar"
},
"dashboard": {
"title": "Hello, {# name #}!",
"rank": "Rank: {# rank #}",
"permissions": "Permssions:",
"buttons": {
"logout": "Logout"
"announcements": {
"table": {
"time": "Time",
"blue": "Blue Team",
"red": "Red Team",
"winner": "Winner",
"notPlayed": "Not Played",
"draw": "Draw",
"team": "Team",
"points": "Points"
}
},
"elo": {
"place": "Place"
},
"warning": {
"title": "This page is not available in your language.",
"text": "The page you are trying to access is not available in your language. You can still access the original page in German."
},
"blog": {
"title": "Announcements - SteamWar"
},
"dashboard": {
"title": "Hello, {# name #}!",
"rank": "Rank: {# rank #}",
"permissions": "Permssions:",
"buttons": {
"logout": "Logout"
},
"stats": {
"playtime": "Playtime: {# playtime #}",
"fights": "Fights: {# fights #}",
"checked": "Accepted Schematics: {# checked #}"
},
"schematic": {
"upload": "Upload",
"cancel": "Cancel",
"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",
"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": {
"playtime": "Playtime: {# playtime #}",
"fights": "Fights: {# fights #}",
"checked": "Accepted Schematics: {# checked #}"
"title": "Fight Statistics"
},
"schematic": {
"upload": "Upload",
"cancel": "Cancel",
"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!"
}
"404": {
"title": "404 - Page not found",
"description": "Page not found"
}
},
"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 NavbarLayout from "@layouts/NavbarLayout.astro";
import {t} from "astro-i18n";
import { t } from "astro-i18n";
import BackgroundImage from "../components/BackgroundImage.astro";
---
<NavbarLayout title={t("login.page")}>
<script>
import {l} from "../util/util";
import {navigate} from "astro:transitions/client";
import {loggedIn} from "../components/repo/authv2";
import {get} from "svelte/store";
import { l } from "../util/util";
import { navigate } from "astro:transitions/client";
import { loggedIn } from "../components/repo/authv2";
import { get } from "svelte/store";
document.addEventListener("astro:page-load", () => {
if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) {
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">
<BackgroundImage />
</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
dark:text-white " style="width: min(100vw, 75em);">
<LoginComponent client:load/>
<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);">
<LoginComponent client:load />
</div>
</NavbarLayout>
</NavbarLayout>