Add support page, admin features, and auth hardening.

Rename public affiliate route to /support with localized nav labels, and include affiliate/social/wallpaper admin, 2FA login, and related site updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Atakan Doğan Özban
2026-07-29 20:25:55 +02:00
co-authored by Cursor
parent 202dc16a52
commit fc45c35dbe
149 changed files with 3496 additions and 11263 deletions
+176
View File
@@ -0,0 +1,176 @@
using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Web;
namespace atakanozbancom.Models.classes
{
public static class TotpHelper
{
private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static string GenerateSecret(int byteLength = 20)
{
var bytes = new byte[byteLength];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(bytes);
}
return ToBase32(bytes);
}
public static bool VerifyCode(string base32Secret, string code, int window = 1)
{
if (string.IsNullOrWhiteSpace(base32Secret) || string.IsNullOrWhiteSpace(code))
return false;
code = code.Trim().Replace(" ", "");
if (code.Length != 6)
return false;
long timestep;
try
{
timestep = GetCurrentTimeStep();
}
catch
{
return false;
}
for (var i = -window; i <= window; i++)
{
var expected = ComputeTotp(base32Secret, timestep + i);
if (FixedTimeEquals(expected, code))
return true;
}
return false;
}
public static string BuildOtpAuthUri(string issuer, string accountName, string base32Secret)
{
var label = HttpUtility.UrlEncode(issuer + ":" + accountName);
var issuerParam = HttpUtility.UrlEncode(issuer);
return string.Format(
CultureInfo.InvariantCulture,
"otpauth://totp/{0}?secret={1}&issuer={2}&digits=6&period=30",
label,
base32Secret,
issuerParam);
}
private static long GetCurrentTimeStep()
{
return (long)Math.Floor((DateTime.UtcNow - UnixEpoch).TotalSeconds / 30.0);
}
private static string ComputeTotp(string base32Secret, long timestep)
{
var key = FromBase32(base32Secret);
var counter = BitConverter.GetBytes(timestep);
if (BitConverter.IsLittleEndian)
Array.Reverse(counter);
byte[] hash;
using (var hmac = new HMACSHA1(key))
{
hash = hmac.ComputeHash(counter);
}
var offset = hash[hash.Length - 1] & 0x0F;
var binary =
((hash[offset] & 0x7F) << 24)
| ((hash[offset + 1] & 0xFF) << 16)
| ((hash[offset + 2] & 0xFF) << 8)
| (hash[offset + 3] & 0xFF);
var otp = binary % 1000000;
return otp.ToString("D6", CultureInfo.InvariantCulture);
}
private static string ToBase32(byte[] data)
{
if (data == null || data.Length == 0)
return string.Empty;
var sb = new StringBuilder((data.Length * 8 + 4) / 5);
int buffer = data[0];
var next = 1;
var bitsLeft = 8;
while (bitsLeft > 0 || next < data.Length)
{
if (bitsLeft < 5)
{
if (next < data.Length)
{
buffer <<= 8;
buffer |= data[next++] & 0xFF;
bitsLeft += 8;
}
else
{
var pad = 5 - bitsLeft;
buffer <<= pad;
bitsLeft += pad;
}
}
var index = (buffer >> (bitsLeft - 5)) & 0x1F;
bitsLeft -= 5;
sb.Append(Base32Alphabet[index]);
}
return sb.ToString();
}
private static byte[] FromBase32(string input)
{
var cleaned = input.Trim().Replace(" ", "").Replace("=", "").ToUpperInvariant();
var output = new byte[cleaned.Length * 5 / 8];
var bitBuffer = 0;
var bitsLeft = 0;
var index = 0;
foreach (var c in cleaned)
{
var val = Base32Alphabet.IndexOf(c);
if (val < 0)
throw new FormatException("Invalid Base32 character.");
bitBuffer = (bitBuffer << 5) | val;
bitsLeft += 5;
if (bitsLeft >= 8)
{
output[index++] = (byte)((bitBuffer >> (bitsLeft - 8)) & 0xFF);
bitsLeft -= 8;
}
}
if (index != output.Length)
{
var trimmed = new byte[index];
Array.Copy(output, trimmed, index);
return trimmed;
}
return output;
}
private static bool FixedTimeEquals(string a, string b)
{
if (a == null || b == null || a.Length != b.Length)
return false;
var diff = 0;
for (var i = 0; i < a.Length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
}
}