Add Backend

This commit is contained in:
BloxerHD 2026-06-23 17:17:57 +01:00
commit 05beab587b
Signed by: bloxerhd018
SSH key fingerprint: SHA256:ItSfcfhpXlfkMK3uQSDYW5X3XKm0hbHWQqWcCK57px8
31 changed files with 59 additions and 6 deletions

77
static/account/index.html Normal file
View file

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Account | SPFN</title>
<link rel="icon" type="image/png" href="/res/img/spfn.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<div style="display: flex;">
<img style="width: 96px; padding: 0px; margin: 0 auto" src="/res/img/spfn.png">
<h2 style="font-size: 32px; margin: auto; padding: 12px">Account</h2>
</div>
<nav>
<ul class="nav-links">
<a href="/"><button class="header-button">Home</button></a>
<a href="/guides"><button class="header-button">Guides</button></a>
<a href="/downloads"><button class="header-button">Downloads</button></a>
<a href="/account"><button class="header-button active">Account</button></a>
</ul>
</nav>
</header>
<main>
<div id="loading-screen">
<h2 class="center">Loading...</h2>
</div>
<div id="user-info" class="info" style="display: none;">
<div id="user-display" class="info-container" style="width: 180px; align-items: center;">
<img src="/res/img/mii-placeholder.png" id="mii-img" style="width: 100%">
<h3 id="display-name" style="margin: 2px;">Display Name</h3>
<p id="sfid" style="margin: 2px;">SFID: Username</p>
<p id="access-level" class="access-level-base">Member</p>
<button style="font-size: 24px; width: 160px;" onclick="logOut()">Log Out</button>
</div>
<div id="user-details" class="info-container" style="flex: 1;">
<p class="info-text" id="email"><strong>Email: </strong>name@domain.com</p>
<p class="info-text" id="dob"><strong>Date of Birth: </strong>dd/mm/yyyy</p>
<hr style="margin: 1rem 0; border: none; height: 1px; background: #aaa;">
<p class="info-text" id="created-at"><strong>Created: </strong>dd/mm/yyyy hh:mm</p>
<hr style="margin: 1rem 0; border: none; height: 1px; background: #aaa;">
<p class="info-text" id="tz"><strong>Timezone: </strong>Europe/London</p>
<p class="info-text" id="region"><strong>Country/Region: </strong>GB</p>
<hr style="margin: 1rem 0; border: none; height: 1px; background: #aaa;">
<button style="font-size: 24px; width: 40%;margin-left: auto;" onclick="showDeleteWarning()">Delete Account</button>
</div>
</div>
<div id="confirm-delete" class="delete-screen" style="display: none">
<h2 style="text-align: center;">WARNING: This Action Is Irreversible</h2>
<p style="text-align: center;">Deleting your account is an irreversible action. You cannot recover your account once this has been done.</p>
<p style="text-align: center;">This will delete your account from our database, not from your Wii U or Cemu. You will need to manually remove it from these.</p>
<div style="display: flex; flex-direction: row; padding: 8px; justify-content: center;">
<button id="confirm-delete-button" style="width: 300px; font-size: 32px;" class="delete-button" onclick="deleteAccount()">Delete Account</button>
<button id="cancel-delete-button" style="width: 300px; font-size: 32px;" onclick="hideDeleteWarning()">Cancel</button>
</div>
</div>
<div id="delete-success" class="delete-screen" style="display: none">
<h2 style="text-align: center; margin: 0;">Account Deleted Successfully</h2>
<p style="text-align: center;">Your account has been removed from our database. You can no longer log in to this account on this website, your Wii U or on Cemu. If you wish to use these services again, you will need to create a new account on your Wii U. Your old SFID can be reused on this new account if it remains unused.</p>
<p style="text-align: center;">This does not remove your account from your devices. You will need to manually remove the files with FTPiiU or by removing the files from your Cemu MLC folder. If you are not sure how to do this, <a href="https://discord.gg/grMSxZf" target="_blank">ask in the discord.</a></p>
</div>
<script type="text/javascript" src="/js/accounts.js"></script>
</main>
<footer>
<p class="center">Need assistance? <a href="https://discord.gg/grMSxZf" target="_blank">Join the Discord</a></p>
</footer>
</body>
</html>

View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Log In | SPFN</title>
<link rel="icon" type="image/png" href="/res/img/spfn.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<div style="display: flex;">
<img style="width: 96px; padding: 0px; margin: 0 auto" src="/res/img/spfn.png">
<h2 style="font-size: 32px; margin: auto; padding: 12px">Account</h2>
</div>
<nav>
<ul class="nav-links">
<a href="/"><button class="header-button">Home</button></a>
<a href="/guides"><button class="header-button">Guides</button></a>
<a href="/downloads"><button class="header-button">Downloads</button></a>
<a href="/account"><button class="header-button active">Account</button></a>
</ul>
</nav>
</header>
<main>
<div id="login-error" class="error-container" style="display: none;">
<h2 id="error-text">ERROR</h2>
</div>
<form id="login">
<h3 class="center">Please log in to access your account.</h3>
<div class="form-group">
<label for="username">SFID / Username:</label>
<input class="login-input" type="text" id="username" name="username" required />
</div>
<div class="form-group">
<label for="password">Password:</label>
<input class="login-input" type="password" id="password" name="password" required />
</div>
<button class="form-button" type="submit">Login</button>
<p class="form-note">Please ask an SPFN Developer in the <a href="https://discord.gg/grMSxZf">discord server</a> for assistance with password resets</p>
</form>
<script type="text/javascript" src="/js/login.js"></script>
</main>
<footer>
<p class="center">Need assistance? <a href="https://discord.gg/grMSxZf" target="_blank">Join the Discord</a></p>
</footer>
</body>

414
static/css/style.css Normal file
View file

@ -0,0 +1,414 @@
body {
background-color: #610707;
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
font-family: 'Noto Sans', sans-serif;
}
button {
cursor: pointer;
border: 2px solid #a00b0b;
border-radius: 10px;
background-color: #6d0606;
color: white;
border-radius: 15px;
}
img {
border-radius: 15px;
margin: 8px 8px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 1rem;
padding: 1rem 2rem;
background-color: #940b0b;
color: white;
border-radius: 15px;
}
footer {
padding: 1rem 2rem;
background-color: #940b0b;
border-radius: 15px;
margin: 1rem;
}
main {
flex: 1;
padding: 1rem;
align-items: center;
height: 100vh;
}
a {
color: #00afff;
}
h1 {
border-radius: 2px;
color: white;
font-size: 64px;
-webkit-text-stroke: 2px black;
margin: 0;
}
strong {
border-radius: 8px;
padding: 4px;
}
.login-input {
background-color: #5f0808;
border: 1px solid white;
color: white;
font-size: 14px;
border-radius: 5px;
padding: 8px;
}
.header-button {
padding: 0.5rem;
font-size: 1rem;
color: white;
cursor: pointer;
height: 64px;
margin: 0px;
font-size: 32px;
}
.center {
text-align: center;
}
form {
max-width: 300px;
margin: 0 auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap:1rem;
border: 4px solid #a00b0b;
border-radius: 15px;
background: #6d0606;
}
.form-group {
display: flex;
flex-direction: column;
font-weight: bold;
margin: 12px 0;
}
.form-button {
font-size: 24px;
width: 100%;
}
.form-note {
text-align: center;
font-size: 12px;
}
@media (orientation: portrait) {
form {
max-width: 100%;
font-size: 48px;
}
.form-button {
font-size: 64px;
}
.login-input {
font-size: 48px;
border-width: 4px;
border-radius: 24px;
}
.form-note {
font-size: 36px;
}
}
.info {
min-width: 40%;
max-width: 800px;
margin: 0 auto;
background-color: #a00b0b;
padding: 4px;
display: flex;
border-radius: 15px;
}
.info-container {
background-color: #6d0606;
text-align: center;
margin: 2px;
border-radius: 15px;
padding: 4px 8px;
display: flex;
flex-direction: column;
}
.info-text {
text-align: left;
margin: 4px;
flex: 1;
}
.guide-buttons {
display: flex;
flex-direction: column;
padding: 0.5rem;
width: 240px;
}
.guide {
font-size: 20px;
height: 48px;
cursor: pointer;
border: 2px solid #a00b0b;
border-radius: 10px;
background-color: #6d0606;
color: white;
margin: 4px 0;
text-align: center;
line-height: 48px;
text-decoration: none;
}
.guide-content {
padding: 0.5rem;
flex: 1;
border: 4px solid #a00b0b;
border-radius: 15px;
color: white;
}
.guide-container {
display: flex;
margin: 0 auto;
width: 70%;
background-color: #770505;
border-radius: 15px;
padding: 8px;
font-size: 16px;
}
@media (orientation: portrait) {
.guide-container {
display: block;
width: 100%;
font-size: 36px;
}
.guide-buttons {
width: 100%;
}
.guide {
font-size: 60px;
height: 128px;
line-height: 128px;
}
}
code {
background-color: #610707;
color: white;
border-radius: 5px;
padding: 2px;
}
.error-container {
background-color: #aa0000;
width: 40%;
margin: 0 auto;
color: white;
text-align: center;
border: 2px solid white;
border-radius: 15px;
}
@media (orientation: portrait) {
.error-container {
font-size: 36px;
width: 100%;
margin: 12px;
}
}
.title {
height: 80vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #770505;
border-radius: 25px;
border: 4px solid #a00b0b;
}
.title-main {
font-size: 84px;
margin: 0 auto;
-webkit-text-stroke: 2px black;
}
.title-desc {
font-size: 24px;
}
.home-container {
display: flex;
flex-direction: column;
align-items: center;
background: #770505;
border-radius: 25px;
margin: 2rem 0;
padding: 24px;
border: 4px solid #a00b0b;
}
.team-member img {
width: 64px;
height: 64px;
max-width: 64px;
max-height: 64px;
object-fit: cover;
flex-shrink: 0;
border-radius: 12px;
}
.member-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
justify-content: center;
}
.team-member {
background-color: #a00b0b;
margin: 8px 8px;
padding: 8px 8px;
border-radius: 15px;
align-items: flex-start;
gap: 12px;
flex: 1 1 280px;
max-width: 600px;
display: flex;
box-sizing: border-box;
min-width: 0;
}
.team-link {
color: white;
}
.splatfest-active {
background-color: #00af00;
border: 2px solid #00ff00;
border-radius: 16px;
padding: 8px;
}
.splatfest-notfound {
background-color: #6d0606;
border: 2px solid #a00b0b;
border-radius: 16px;
padding: 8px;
}
.delete-screen {
background-color: #6d0606;
border: 6px solid #a00b0b;
border-radius: 16px;
width: 60%;
margin: 0 auto;
padding: 8px;
}
.delete-button {
background-color: #dc0404;
border: none;
padding: 8px;
margin: 0 16px;
}
.access-level-base {
background-color: #047205;
border-style: solid;
border-color: #00a003;
border-radius: 8px;
border-width: 2px;
width: 90%;
}
.download-category {
margin: 12px;
padding: 12px;
}
.download-category-title {
margin: 0;
margin-bottom: 12px;
}
.download-container {
display: flex;
margin: 0 auto;
width: 70%;
background-color: #770505;
border-radius: 15px;
padding: 8px;
font-size: 16px;
flex-direction: column;
text-align: center;
}
.download-link-container {
display: flex;
flex-direction: row;
justify-content: center;
}
.download-link {
border-radius: 15px;
padding: 16px;
border: 4px solid #a00b0b;
margin: 0 8px;
}
.download-title {
padding-bottom: 8px;
margin: 0;
}
.download-path {
color: rgba(255, 255, 255, 0.7);
margin: 0;
padding-bottom: 8px;
font-size: 0.9rem;
}
@media (orientation: portrait) {
.download-container {
display: block;
width: 100%;
font-size: 36px;
}
}
.active {
background-color: #a00b0b;
border-color: #6d0606;
}

View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Downloads | SPFN</title>
<link rel="icon" type="image/png" href="/res/img/spfn.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<div style="display: flex;">
<img style="width: 96px; padding: 0px; margin: 0px" src="/res/img/spfn.png">
<h2 style="font-size: 32px; margin: auto; padding: 12px">Downloads</h2>
</div>
<nav>
<ul class="nav-links">
<a href="/"><button class="header-button">Home</button></a>
<a href="/guides"><button class="header-button">Guides</button></a>
<a href="/downloads"><button class="header-button active">Downloads</button></a>
<a href="/account"><button class="header-button">Account</button></a>
</ul>
</nav>
</header>
<main>
<div class="download-container">
<div>
<div class="download-category">
<h2 class="download-category-title">Wii U</h2>
<div class="download-link-container">
<div class="download-link">
<h3 class="download-title">Inkay-spfn.wps</h3>
<p class="download-path">SD:/wiiu/environments/SPFN/plugins/Inkay-spfn.wps</p>
<a download href="https://git.spbr.net/spacebar/inkay/releases/download/latest/Inkay-spfn.wps"><button class="header-button">Download</button></a>
</div>
<div class="download-link">
<h3 class="download-title">Inkay-spfn.wms</h3>
<p class="download-path">SD:/wiiu/environments/SPFN/modules/Inkay-spfn.wms</p>
<a download href="https://git.spbr.net/spacebar/inkay/releases/download/latest/Inkay-spfn.wms"><button class="header-button">Download</button></a>
</div>
</div>
</div>
<div class="download-category">
<h2 class="download-category-title">Cemu</h2>
<div class="download-link-container">
<div class="download-link">
<h3 class="download-title">network_services.xml</h3>
<p class="download-path">%appdata%/Cemu/network_services.xml</p>
<a download href="/downloads/network_services.xml"><button class="header-button">Download</button></a>
</div>
</div>
</div>
</div>
</div>
</main>
<footer>
<p class="center">Need assistance? <a href="https://discord.gg/grMSxZf" target="_blank">Join the Discord</a></p>
</footer>
</body>
</html>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<content>
<networkname>SPFN</networkname>
<disablesslverification>1</disablesslverification>
<urls>
<act>https://account.spfn.net</act>
<ecs>https://ecs.wup.shop.nintendo.net/ecs/services/ECommerceSOAP</ecs>
<nus>https://nus.wup.shop.nintendo.net/nus/services/NetUpdateSOAP</nus>
<ias>https://ias.wup.shop.nintendo.net/ias/services/IdentityAuthenticationSOAP</ias>
<ccsu>https://ccs.wup.shop.nintendo.net/ccs/download</ccsu>
<ccs>http://ccs.cdn.wup.shop.nintendo.net/ccs/download</ccs>
<idbe>https://idbe-wup.cdn.nintendo.net/icondata</idbe>
<boss>https://boss.spfn.net/p01/tasksheet</boss>
<tagaya>https://tagaya.wup.shop.nintendo.net/tagaya/versionlist</tagaya>
<olv>https://discover.olv.nintendo.net/v1/endpoint</olv>
</urls>
</content>

53
static/guides/index.html Normal file
View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Guides | SPFN</title>
<link rel="icon" type="image/png" href="/res/img/spfn.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</head>
<body>
<header>
<div style="display: flex;">
<img style="width: 96px; padding: 0px; margin: 0 auto" src="/res/img/spfn.png">
<h2 style="font-size: 32px; margin: auto; padding: 12px">Guides</h2>
</div>
<nav>
<ul class="nav-links">
<a href="/"><button class="header-button">Home</button></a>
<a href="/guides"><button class="header-button active">Guides</button></a>
<a href="/downloads"><button class="header-button">Downloads</button></a>
<a href="/account"><button class="header-button">Account</button></a>
</ul>
</nav>
</header>
<main>
<div class="guide-container">
<div class="guide-buttons">
<a class="guide" id="install-wiiu" href="/guides?guide=install-wiiu">Installation - Wii U</a>
<a class="guide" id="install-cemu" href="/guides?guide=install-cemu">Installation - Cemu</a>
<a class="guide" id="common-errors" href="/guides?guide=common-errors">Common Error Codes</a>
<a class="guide" id="troubleshoot-wiiu" href="/guides?guide=troubleshoot-wiiu">Troubleshooting - Wii U</a>
<a class="guide" id="troubleshoot-cemu" href="/guides?guide=troubleshoot-cemu">Troubleshooting - Cemu</a>
</div>
<div class="guide-content" id="guide-contents">
<h3>Loading...</h3>
</div>
</div>
<script type="text/javascript" src="/js/guides.js"></script>
</main>
<footer>
<p class="center">Need assistance? <a href="https://discord.gg/grMSxZf" target="_blank">Join the Discord</a></p>
</footer>
</body>
</html>

131
static/index.html Normal file
View file

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home | SPFN</title>
<link rel="icon" type="image/png" href="/res/img/spfn.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<div style="display: flex;">
<img style="width: 96px; padding: 0px; margin: 0px" src="/res/img/spfn.png">
<h2 style="font-size: 32px; margin: auto; padding: 12px">Splatfestival Network</h2>
</div>
<nav>
<ul class="nav-links">
<a href="/"><button class="header-button active">Home</button></a>
<a href="/guides"><button class="header-button">Guides</button></a>
<a href="/downloads"><button class="header-button">Downloads</button></a>
<a href="/account"><button class="header-button">Account</button></a>
</ul>
</nav>
</header>
<main>
<div class="title">
<img style="width: 192px; padding: 0px; margin: 0px" src="/res/img/spfn.png">
<h2 class="title-main">Splatfestival Network</h2>
<p class="title-desc">Restoring Splatfests to Splatoon on the Wii U</p>
</div>
<div class="home-container">
<h2>Where to Find Us</h2>
<p class="center">
Join the Splatfestival discord server for updates to the
servers and news about current or future Splatfests.
</p>
<p>
Our code is also all open source,
which can all be found on our self-hosted Forgejo instance.
</p>
<div style="display: flex; flex-direction: row;">
<a href="https://discord.gg/grMSxZf" target="_blank"><img src="/res/img/discord.png" style="width: 8rem;" /></a>
<a href="https://git.spbr.net/spacebar" target="_blank"><img src="/res/img/fj.png" style="width: 8rem;" /></a>
</div>
</div>
<div class="home-container" style="display: none;">
<h2 style="margin: 4px;">Splatfest Information</h2>
<p style="margin: 4px;">Check here for information about current or upcoming splatfests</p>
<div id="splatfest-active" class="splatfest-active", style="display: none;">
<h3 style="margin: 0px;">Splatfest is Active!</h3>
</div>
<div id="splatfest-notfound" class="splatfest-notfound">
<h3 style="margin: 0px;">No splatfests currently active or upcoming</h3>
</div>
<div id="splatfest-info" style="text-align: center; display: none;">
<h3 id="splatfest-title"><strong style="background-color: #e25604;">Team Kid</strong> vs. <strong style="background-color: #0438e2;">Team Squid</strong></h3>
<p id="splatfest-starts"><strong>Starts: </strong>dd/mm/yyyy hh/mm</p>
<p id="splatfest-ends"><strong>Ends: </strong>dd/mm/yyyy hh/mm</p>
</div>
</div>
<div class="home-container">
<h2 style="margin: 0 auto;">Credits</h2>
<p>Members or Groups that made Splatfestival Network what it is today</p>
<div class="team-members">
<div class="member-row">
<div class="team-member">
<img src="/res/img/pretendo.png">
<div style="display: block;">
<a href="https://github.com/PretendoNetwork/" target="_blank" class="team-link"><h2>Pretendo Network</h2></a>
<p>Original Server Code and Reverse Engineering</p>
</div>
</div>
<div class="team-member">
<img src="/res/img/rusticmaple.png">
<div style="display: block;">
<a href="https://github.com/RusticMaple" target="_blank" class="team-link"><h2>RusticMaple</h2></a>
<p>Owner and Server Developer</p>
</div>
</div>
<div class="team-member">
<img src="/res/img/ceantix.png">
<div style="display: block;">
<a href="https://github.com/ceantixdev" target="_blank" class="team-link"><h2>Ceantix</h2></a>
<p>Server Developer</p>
</div>
</div>
</div>
<div class="member-row">
<div class="team-member">
<img src="/res/img/bloxerhd.png">
<div style="display: block;">
<a href="https://github.com/BloxerHD018" target="_blank" class="team-link"><h2>BloxerHD</h2></a>
<p>Website and Server Developer</p>
</div>
</div>
<div class="team-member">
<img src="/res/img/kinnay.png">
<div style="display: block;">
<a href="https://github.com/kinnay" target="_blank" class="team-link"><h2>Kinnay</h2></a>
<p>Documentation of PRUDP and RMC</p>
</div>
</div>
<div class="team-member">
<img src="/res/img/purplepote.png">
<div style="display: block;">
<h2>PurplePote</h2>
<p>Splatfestival Admin who Supported Development</p>
</div>
</div>
</div>
<div class="member-row">
<div class="team-member">
<img src="/res/img/splatfestival.png">
<div style="display: block;">
<a href="https://discord.gg/grMSxZf" target="_blank" class="team-link"><h2>Splatfestival Staff</h2></a>
<p>Supported Development and Deployment of SPFN</p>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="/js/home.js"></script>
</main>
<footer>
<p class="center">Need assistance? <a href="https://discord.gg/grMSxZf" target="_blank">Join the Discord</a></p>
</footer>
</body>
</html>

115
static/js/accounts.js Normal file
View file

@ -0,0 +1,115 @@
let userAccessLevels = { // Name | Text Colour | Background Colour | Border Colour
"-3": ["Banned", "#fff", "#2e2e2e", "#727272"],
"-2": ["Banned", "#fff", "#2e2e2e", "#727272"],
"-1": ["Banned", "#fff", "#2e2e2e", "#727272"],
"0": ["Member", "#fff", "#047205", "#00a003"],
"1": ["Tester", "#fff", "#0c98a2", "#01c9d7"],
"2": ["Moderator", "#000", "#00dc04", "#067708"],
"3": ["Admin", "#fff", "#920606", "#c80404"],
}
function updateUserDataDisplay(data) {
document.getElementById("display-name").textContent = data["mii"]["name"];
document.getElementById("sfid").textContent = `SFID: ${data["user_id"]}`;
document.getElementById("email").innerHTML = `<strong>Email: </strong>${data["email"]["address"]}`;
document.getElementById("dob").innerHTML = `<strong>Date of Birth: </strong>${data["birth_date"]}`;
let createdAt = new Date(data["create_date"] + "Z");
document.getElementById("created-at").innerHTML = `<strong>Created: </strong>${createdAt.toLocaleString()}`
document.getElementById("tz").innerHTML = `<strong>Timezone: </strong>${data["tz_name"]}`;
document.getElementById("region").innerHTML = `<strong>Country/Region: </strong>${data["country"]}`;
document.getElementById("user-info").style.display = "flex";
let miiLink = `https://mii.spfn.net/${data["pid"]}/main.png`
document.getElementById("mii-img").src = miiLink;
let level = userAccessLevels[data["account_level"]];
if (!level) level = ["Unknown", "#fff", "#000000ff", "#ffffffff"]; // Refer to comment on userAccessLevels for a formatting guide
document.getElementById("access-level").textContent = level[0];
document.getElementById("access-level").style = `color: ${level[1]}; background-color: ${level[2]}; border-color: ${level[3]}`;
}
function logOut() {
sessionStorage.clear();
window.location.href = "/account/login?redirect=/account"
}
// Check if there is an active login
window.onload = async function() {
let expiryStr = sessionStorage.getItem("authExpires");
if (expiryStr) {
let expiry = new Date(parseInt(expiryStr));
if (expiry > new Date()) { // Hasn't expired - Get user data
let token = sessionStorage.getItem("authToken");
const response = await fetch("https://account.spfn.net/api/v2/users/@me/profile", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
}
})
if (!response.ok) throw new Error("Network Response was not okay when requesting Profile")
const data = await response.json();
updateUserDataDisplay(data);
document.getElementById("loading-screen").style.display = "none";
} else { // Has expired - Prompt user to log back in
console.log("Token Expired")
//window.location.href = "/account/login?redirect=/account"
}
} else { // User has never logged in for this session
window.location.href = "/account/login?redirect=/account"
}
}
function showDeleteWarning() {
document.getElementById("confirm-delete").style = ""
document.getElementById("user-info").style = "display: none;"
}
function hideDeleteWarning() {
document.getElementById("user-info").style = ""
document.getElementById("confirm-delete").style = "display: none;"
}
async function deleteAccount() {
let token;
let expiryStr = sessionStorage.getItem("authExpires");
if (expiryStr) { // Expiry exists so token should exist
token = sessionStorage.getItem("authToken");
let expiry = new Date(expiryStr);
if (expiry < new Date()) { // Expired token
window.location.href = "/account/login?redirect=/account"
} else if (!token) { // Expiry Saved but No Token (shouldn't be possible but it'll be caught if it happens)
window.location.href = "/account/login?redirect=/account"
}
} else { // Token Never Saved in Session
window.location.href = "/account/login?redirect=/account"
}
const response = await fetch("https://account.spfn.net/api/v2/users/@me/delete", {
method: "GET",
headers: {
"Authorization": `Bearer ${token}`
}
})
if (!response.ok) {
loginError(await response.text(), response.status);
throw new Error("Network Response was not okay when requesting Account Deletion");
}
document.getElementById("confirm-delete").style = "display: none;"
document.getElementById("delete-success").style = ""
}

26
static/js/guides.js Normal file
View file

@ -0,0 +1,26 @@
const guideContents = document.querySelectorAll(".guide-content");
const guideButtons = document.querySelectorAll(".guide");
const params = new URLSearchParams(window.location.search);
const guideParam = params.get("guide");
let guide = "install-wiiu";
let guidePath = '/md/install-wiiu.md';
if (guideParam) {
guide = guideParam;
guidePath = `/md/${guide}.md`;
}
fetch(guidePath)
.then(res => {
if (!res.ok) throw new Error(`Guide "${guide} was not found`);
const guideButton = document.getElementById(guide);
if (guideButton) guideButton.classList.add("active");
return res.text();
})
.then(text => {
const html = marked.parse(text);
document.getElementById("guide-contents").innerHTML = html;
})

86
static/js/home.js Normal file
View file

@ -0,0 +1,86 @@
async function requestFestInfo() {
// DO NOT USE THIS - USE ROUTE https://boss-info.spfn.net/api/v2/fest-info
const response = await fetch("https://account.spfn.net/api/v2/fest-info", { // ROUTE NOT IMPLEMENTED
method: "GET"
})
if (!response.ok) return;
/*
{
"active",
"a-name",
"a-hex",
"b-name",
"b-hex",
"start",
"end"
}
*/
const data = await response.json()
return data;
}
/* Disabled until actually implemented
window.onload = async function() {
// Verify Fest Information
let data = localStorage.getItem("fest-info");
if (data) { // Fest data previously stored
const end = new Date(data["end"] + "Z")
const now = new Date()
if (end < now) { // Fest is over - Remove and check for new fest
localStorage.removeItem("fest-info");
data = await requestFestInfo();
if (data) {
localStorage.setItem("fest-info", data);
}
} else { // Fest is either Upcoming or Active - Update active state
const start = new Date(data["start"] + "Z");
const active = start > now && end < now;
if (data["active"] != active) {
data["active"] = active;
localStorage.setItem("fest-data", data);
}
}
} else { // No saved data - Check for any
data = await requestFestInfo();
if (data) {
localStorage.setItem("fest-info", data);
}
}
if (data) { // Active or Upcoming Fest Found
document.getElementById("splatfest-notfound").style.display = "none";
let festTitle = `<strong style="background-color: ${data["a-hex"]};">${data["a-name"]}</strong> vs. <strong style="background-color: ${data["b-hex"]};">${data["b-name"]}</strong>`;
document.getElementById("splatfest-title").innerHTML = festTitle;
let start = new Date(data["start"] + "Z");
let startStr = `<strong>Starts: </strong>${start.toLocaleString()}`;
document.getElementById("splatfest-starts").innerHTML = startStr;
let end = new Date(data["end"] + "Z");
let endStr = `<strong>Ends: </strong>${end.toLocaleString()}`;
document.getElementById("splatfest-ends").innerHTML = endStr;
let now = new Date();
if (start > now && end < now) { // Fest is Active
console.log("Active fest found!")
} else { // Fest is Upcoming
console.log("Upcoming fest found!")
}
} else { // No fest
document.getElementById("splatfest-info").style.display = "none";
console.log("No fests currently planned or active")
}
}
*/

99
static/js/login.js Normal file
View file

@ -0,0 +1,99 @@
function loginError(message, code = null) {
let errorStr;
if (code) {
errorStr = `Status code ${code}: ${message}`;
} else {
errorStr = message;
}
document.getElementById("error-text").textContent = errorStr;
document.getElementById("login-error").style.display = "block";
}
async function generateToken(username, password) {
const credentials = btoa(`${username} ${password}`);
const body = `grant_type=password&username=${username}&password=${password}&client_id=website`
let response;
try {
response = await fetch("https://account.spfn.net/api/v2/oauth2/generate_token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body,
})
} catch (err) {
loginError(`Internal Server Error: ${err.message}`)
throw new Error(err);
}
if (!response.ok) {
if (response.status == 400) { // Invalid Login
loginError("Invalid SFID or Password");
} else {
loginError(await response.text(), response.status);
}
throw new Error("Network Response was not okay when Generating Token");
};
const data = await response.json();
sessionStorage.setItem("authToken", data["access_token"]);
const expiry = Date.now() + (data["expires_in"] * 1000);
sessionStorage.setItem("authExpires", expiry)
return data["access_token"];
}
async function getToken(username, password) {
let token = sessionStorage.getItem("authToken");
let expiryStr = sessionStorage.getItem("authExpires");
if (expiryStr) { // Expiry exists so token should exist
let expiry = new Date(expiryStr);
if (expiry < new Date()) { // Expired token
token = await generateToken(username, password);
} else if (!token) { // Expiry Saved but No Token (shouldn't be possible but it'll be caught if it happens)
token = await generateToken(username, password);
}
} else { // Token Never Saved in Session
token = await generateToken(username, password);
}
return token
}
document.getElementById("login").addEventListener("submit", async function(event) {
event.preventDefault();
document.getElementById("login-error").style.display = "none";
const username = await document.getElementById("username").value;
const password = await document.getElementById("password").value;
let token = await getToken(username, password);
if (!token) return;
document.getElementById("password").value = "";
// Go Back to Origin Page
const params = new URLSearchParams(window.location.search);
const redirectURL = params.get("redirect")
window.location.href = redirectURL;
})
window.onload = async function () { // Check if the token expired
let expiryStr = sessionStorage.getItem("authExpires");
if (expiryStr) {
let expiry = new Date(expiryStr);
if (expiry < new Date()) { // Expired - Tell the user it expired
loginError("Login Expired - Please Log In Again")
}
}
}

View file

@ -0,0 +1,19 @@
## Common Error Codes
---
### 118-XXXX when Playing Matches or Matchmaking
These errors refer to the Peer to Peer connection. Either your connection or the connection of the host console is unstable, resulting in these errors. Ensure that your NAT type is either A or B (preferrably A) and try again
### 686-XXXX when Opening SPFN Environment
SPFN requires you to be on the latest version of Aroma. Open the Aroma Updater while in the SPFN environment and update it.
### 102-28XX (Banned Error)
Either you or someone on your Wii U was banned from SPFN. Appeals are currently unavailable. Do not request an appeal from SPFN or Splatfestival staff. When appeals are available, submit your SFID and why you think it was a false ban and wait for a response.
### My error isn't listed here
[Join the Discord](https://discord.gg/grMSxZf) and ask in the assistance channel

39
static/md/install-cemu.md Normal file
View file

@ -0,0 +1,39 @@
## Installation - Cemu
---
Requirements:
- Wii U with SPFN set up
- SD Card (Preferrably 32GB)
- Computer or other device to access SD Card
- Device that can run Cemu
- An internet connection
---
### Step 1: Dump Necessary Files
This guide requires that you have already set up SPFN on your Wii U and created an account. Account creation is currently unavailable on this website. If you have not done this yet, [follow this guide](/guides?guide=install-wiiu)
You will also need to dump your SPFN account and `otp.bin` and `seeprom.bin`files from your Wii U. Follow Cemu's [guide to dump online files](https://cemu.cfw.guide/using-dumpling.html#online-files) and dump your SPFN account. Take this time to also dump your Splatoon game if you haven't already done so.
Transfer the dumped files to your computer.
---
### Step 2: Cemu Configuration
Make sure you are using the latest version of Cemu. This can be found [here](https://github.com/cemu-project/Cemu/releases).
Open Cemu and go to `File -> Open Cemu Folder`. Place the dumped `mlc01` folder, the `otp.bin` file and the `seeprom.bin` file into the Cemu folder.
Download network_services.xml from [here](/downloads/) and place it into the Cemu folder
Open Cemu and go to `Options -> General Settings`. In the `Account` tab, make sure your SPFN account is selected. If not, select it from the dropdown.
You should see the option to switch to Custom network services. Select this. If you can't select it, you haven't added the `network_services.xml` file correctly.
Open Splatoon and check that you get the news.
You are now ready to play on the Splatfestival Network! [Join the Discord](https://discord.gg/grMSxZf) for information about upcoming or active Splatfests and other updates.

62
static/md/install-wiiu.md Normal file
View file

@ -0,0 +1,62 @@
## Installation - Wii U
---
Requirements:
- Wii U with Aroma
- SD Card (Preferrably 32GB)
- Computer or other device to access SD Card
- An internet connection
In order to follow this guide, you must have a modded Wii U console with the latest version of Aroma. If your console isn't modded, [follow this guide](https://wiiu.hacks.guide/). If your console is modded, check for any Aroma updates with the Aroma Updater and install it if any are found.
This guide will create a duplicate Aroma environment which you can use to connect to our servers. Once set up, you will be able to switch between Pretendo and SPFN by switching which environment you launch into when starting your Wii U.
Ensure that you don't have a default user set before setting up SPFN as it could cause you to log in with the wrong account. To disable it, turn on your Wii U and when it logs in, press your Mii in the top left and scroll down to the option labelled "Default User" and disable it.
If you are currently using Pretendo's DNS servers to connect to Pretendo, disable the DNS server before continuing.
---
### Step 1: SD Card Preparation
Plug your SD card into your computer and navigate to `SD:/wiiu/environments`
Make a copy of your `aroma` environment folder and rename it to `SPFN` - this will be the environment you use for connecting to our servers.
For users who previously used Pretendo's Inkay plugin, delete the files `plugins/Inkay-pretendo.wps` and `modules/Inkay-pretendo.wms` from the SPFN environment.
Download the plugins and modules found on the [Downloads page](/downloads/) - make sure you enable file name extensions in your file browser and take note of the extension.
- `Inkay-spfn.wps` goes into the `plugins` folder
- `Inkay-spfn.wms` goes into the `modules` folder
With these files installed, you can eject your SD card and put it into your Wii U.
---
### Step 2: Setup on Console
Power on the console and hold `X` on the gamepad to open the Environment Loader.
Navigate using the D-Pad to `SPFN` and press `A` to select. Your console will now load into the environment and connect to our servers.
In the top left of your screen, you should see a message saying "Using SPFN".
> If you see "Using Pretendo Network", DO NOT PROGRESS. Turn off the console and delete the Pretendo plugins as mentioned in step 1.
> If you do not see the "Using SPFN" message, you likely didn't install the plugin correctly. Check the locations of the `Inkay-spfn.wps` and `Inkay-spfn.wms` files.
SPFN's servers are separate from both Nintendo and Pretendo, so you will need to create a new account to access our services. Now that your console is connected to our servers, you should be brought to the accounts page. Press `+` on your gamepad to create a new account. Follow the process of creating the account. Select no when prompted if you have an NNID. Select a Mii for your account. When asked to create an account, press Link.
Before continuing, confirm that the agreement references Splatfestival Network. If it does not, your plugins aren't set up correctly. Return to step 1 and ensure they are correctly placed.
If it does reference Splatfestival Network, read through the agreement and press `Agree`, then enter your account information.
> DO NOT REUSE PASSWORDS ACROSS MULTIPLE ACCOUNTS INSIDE OR OUTSIDE OF OUR NETWORK
Once you have created your account, you will be sent a verification code to your registered email address. It may show up in your spam folder, so look around for it before resending a new one. To enter it, log into your account on your Wii U and it will prompt you for the code. If you are already logged in, press on your Mii in the top left of your gamepad and select Switch User. Then, select your SPFN account to log back in. The prompt should appear.
Open Splatoon and check that you get the news.
You are now ready to play on the Splatfestival Network! [Join the Discord](https://discord.gg/grMSxZf) for information about upcoming or active Splatfests and other updates.

View file

@ -0,0 +1,11 @@
## Troubleshooting - Cemu
---
### Splatoon freezes on the loading screen when first opening
Restart Cemu and go to `Debug -> Open Logging Window`. Keep this window open and launch the game. Wait for at least 30 seconds. Screenshot the logging window and send it to the assistance channel of the [Discord](https://discord.gg/grMSxZf).
### My issue isn't listed here
[Join the Discord](https://discord.gg/grMSxZf) and ask in the assistance channel

View file

@ -0,0 +1,10 @@
## Troubleshooting - Wii U
---
### When I load the environment, I don't see a "Using SPFN" message / see a "Using Pretendo Network" message
Either you have selected your main Aroma environment or you have not set up the plugin correctly. Verify that you have removed the Pretendo files from the SPFN environment folder (`inkay-pretendo.wps` in `plugins` and `inkay-pretendo.wms` in `modules`). Then, when next booting your Wii U, ensure you hold `X` to open the environment selector and select the SPFN environment.
### My issue isn't listed here
[Join the Discord](https://discord.gg/grMSxZf) and ask in the assistance channel

BIN
static/res/img/bloxerhd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/res/img/ceantix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
static/res/img/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

BIN
static/res/img/fj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
static/res/img/kinnay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/res/img/pretendo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

BIN
static/res/img/spfn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB