Server Saving

Een project om spelersprofielen veilig extern op te slaan, zodat spelers op elk apparaat verder kunnen spelen.

Hoe het werkt in 4 stappen

  1. Je registreert of logt in in de game.
  2. De server geeft een tijdelijke toegangscode (JWT token).
  3. Met die token kan de game je profiel opslaan en laden.
  4. Je profiel wordt versleuteld opgeslagen in de database.

Kernfeatures / Highlights

  • Beveiliging:
    • Inloggen met bcrypt‑gehashte wachtwoorden
    • JWT tokens (verlopen na 2 uur)
    • Profielen versleuteld met AES‑256‑GCM (authenticatie + integriteit)
  • Datamodel:
    • Users en Players tabellen
    • Unieke combinatie user_id + name
    • Profielnaam‑normalisatie (spaties/hoofdletters/tekens eenduidig)
  • API:
    • Endpoints voor register, login, save, load, list en delete
  • Unity client:
    • TMP‑UI en standaard UI voorbeelden
    • Save/Load/List met UnityWebRequest
    • Schermmeldingen en foutafhandeling

Code highlights

  1. Server – versleutelen/ontsleutelen

  • Laat zien dat profielen niet in plain text op de server staan en hoe IV + auth tag worden beheerd.
				
					// server/server.js
function encrypt(plain) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv("aes-256-gcm", ENCRYPTION_KEY, iv);
  let enc = cipher.update(plain, "utf8", "hex");
  enc += cipher.final("hex");
  const tag = cipher.getAuthTag();
  return `${iv.toString("hex")}:${tag.toString("hex")}:${enc}`;
}

function decrypt(blob) {
  const [ivHex, tagHex, dataHex] = blob.split(":");
  const iv = Buffer.from(ivHex, "hex");
  const tag = Buffer.from(tagHex, "hex");
  const decipher = crypto.createDecipheriv("aes-256-gcm", ENCRYPTION_KEY, iv);
  decipher.setAuthTag(tag);
  let dec = decipher.update(dataHex, "hex", "utf8");
  dec += decipher.final("utf8");
  return dec;
}
				
			
  1. Server – profielnaam normaliseren + auth middleware

  • Toont input‑sanitatie en toegangscontrole zonder veel boilerplate.
				
					// server/server.js
function normalizeName(raw) {
  if (typeof raw !== "string") return null;
  let s = raw.replace(/\+/g, " ");
  try { s = decodeURIComponent(s); } catch {}
  s = s.trim().toLowerCase();
  if (s.length === 0 || s.length > 32) return null;
  return /^[a-z0-9 _-]+$/.test(s) ? s : null;
}

function authenticate(req, res, next) {
  const header = req.headers.authorization;
  if (!header) return res.status(401).json({ error: "Missing token" });
  const [type, token] = header.split(" ");
  if (type !== "Bearer" || !token) return res.status(401).json({ error: "Invalid auth header" });
  try {
    req.user = jwt.verify(token, JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
}
				
			
  1. Server – opslaan van een profiel (upsert + versleuteling)

  • Concreet hoe data veilig wordt opgeslagen en hoe dubbele profielen netjes worden geüpdatet.
				
					// server/server.js
app.post("/player/:name", authenticate, (req, res) => {
  const user_id = req.user.user_id;
  const nameKey = normalizeName(req.params.name);
  if (!nameKey) return res.status(400).json({ error: "Invalid profile name" });

  let { money, level } = req.body || {};
  money = Number.isFinite(+money) ? +money : 0;
  level = Number.isFinite(+level) ? +level : 1;

  const encrypted = encrypt(JSON.stringify({ money, level }));

  db.run(
    `INSERT INTO players (user_id, name, data)
     VALUES (?, ?, ?)
     ON CONFLICT(user_id, name) DO UPDATE SET data=excluded.data`,
    [user_id, nameKey, encrypted],
    (err) => err ? res.status(500).json({ error: err.message }) : res.json({ success: true })
  );
});
				
			
  1. Unity client – inloggen en token opslaan

  • Laat zien hoe de client een token krijgt en UI‑toegang vrijgeeft na inloggen.
				
					// server-saving/Assets/Scripts/SecureServerClientTMP.cs
IEnumerator LoginCoroutine(string username, string password)
{
    string url = baseUrl.TrimEnd('/') + "/auth/login";
    string json = $"{{\"username\":\"{username}\",\"password\":\"{password}\"}}";

    using (UnityWebRequest req = new UnityWebRequest(url, "POST"))
    {
        req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
        req.downloadHandler = new DownloadHandlerBuffer();
        req.SetRequestHeader("Content-Type", "application/json");

        yield return req.SendWebRequest();

        if (req.result == UnityWebRequest.Result.Success)
        {
            var resp = JsonUtility.FromJson<LoginResponse>(req.downloadHandler.text);
            jwtToken = resp?.token ?? "";
            SetActionsInteractable(!string.IsNullOrEmpty(jwtToken));
            SetInfo(string.IsNullOrEmpty(jwtToken) ? "Login failed" : "Login success!");
        }
        else
        {
            SetInfo($"Login error: {req.responseCode} {req.error} {req.downloadHandler.text}");
            SetActionsInteractable(false);
        }
    }
}
				
			
  1. Unity client – save profiel met Bearer token

  • Kort en zelfstandig: toont de essentie van authenticated save.
				
					// server-saving/Assets/Scripts/SecureServerClientTMP.cs
IEnumerator SavePlayerDataCoroutine(string playerName, int money, int level)
{
    if (string.IsNullOrWhiteSpace(jwtToken)) { SetInfo("Login eerst."); yield break; }

    string url = baseUrl.TrimEnd('/') + "/player/" + UnityWebRequest.EscapeURL(playerName);
    string json = $"{{\"money\":{money},\"level\":{level}}}";

    using (UnityWebRequest req = new UnityWebRequest(url, "POST"))
    {
        req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
        req.downloadHandler = new DownloadHandlerBuffer();
        req.SetRequestHeader("Content-Type", "application/json");
        req.SetRequestHeader("Authorization", "Bearer " + jwtToken);

        yield return req.SendWebRequest();
        SetInfo(req.result == UnityWebRequest.Result.Success ? "Data saved!" :
                $"Save error: {req.responseCode} {req.error} {req.downloadHandler.text}");
    }
}