feat(auth): Better Auth, kullanıcı adı ile giriş, DAL ve proxy

- MongoDB/Mongoose bağlantıları, kayıt kapalı, admin + username plugin, rate limit
- DAL: getCurrentUser, requireSession, requireRole, getActionUser
- Kullanıcı adı normalizasyonu: İ/I/ı → i (tr-TR 'ADMIN' → 'admın' hatası)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 02:02:00 +03:00
parent 381214c2cb
commit 5191a5cce1
14 changed files with 319 additions and 4 deletions
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { isPlaceholderEmail, placeholderEmail, storedEmail, visibleEmail } from "./account-email";
describe("yer tutucu e-posta", () => {
it("kullanıcı adından .invalid adres üretir ve tanır", () => {
const e = placeholderEmail("emre");
expect(e).toBe("emre@kullanici.butcem.invalid");
expect(isPlaceholderEmail(e)).toBe(true);
expect(isPlaceholderEmail("EMRE@KULLANICI.BUTCEM.INVALID")).toBe(true);
expect(isPlaceholderEmail("emre@ornek.com")).toBe(false);
expect(isPlaceholderEmail(undefined)).toBe(false);
});
it("kaydedilen ve gösterilen e-posta", () => {
expect(storedEmail("emre", undefined)).toBe("emre@kullanici.butcem.invalid");
expect(storedEmail("ayse", "ayse@ornek.com")).toBe("ayse@ornek.com");
expect(visibleEmail("emre@kullanici.butcem.invalid")).toBeUndefined();
expect(visibleEmail("ayse@ornek.com")).toBe("ayse@ornek.com");
});
});
+24
View File
@@ -0,0 +1,24 @@
/**
* E-postası olmayan kullanıcılar (ör. çocuklar) kullanıcı adıyla giriş yapar. Better Auth
* e-postayı zorunlu tuttuğu için bunlara ayrılmış `.invalid` alan adında (RFC 2606)
* yer tutucu bir adres verilir; arayüzde "e-posta yok" olarak gösterilir.
*/
export const PLACEHOLDER_EMAIL_DOMAIN = "kullanici.butcem.invalid";
export function placeholderEmail(username: string): string {
return `${username}@${PLACEHOLDER_EMAIL_DOMAIN}`;
}
export function isPlaceholderEmail(email: string | null | undefined): boolean {
return Boolean(email?.toLowerCase().endsWith(`@${PLACEHOLDER_EMAIL_DOMAIN}`));
}
/** Kaydedilecek e-posta: girilmişse o, yoksa kullanıcı adından yer tutucu. */
export function storedEmail(username: string, email: string | undefined): string {
return email ?? placeholderEmail(username);
}
/** Gösterilecek e-posta: yer tutucuysa undefined. */
export function visibleEmail(email: string | null | undefined): string | undefined {
return email && !isPlaceholderEmail(email) ? email : undefined;
}
+9 -3
View File
@@ -12,11 +12,17 @@ export const USERNAME_PATTERN = /^[a-z0-9._]{3,30}$/;
export const nameSchema = z.string().trim().min(2, v.nameMin).max(80, v.nameMax);
/** Kullanıcı adı küçük harfe çevrilir; Türkçe büyük İ/I sorununu önlemek için tr-TR kullanılır. */
/**
* Kullanıcı adı normalizasyonu (ASCII). Türkçe yerel ayarı KULLANILMAZ: tr-TR'de "I" → "ı" olur
* ve "ADMIN" "admın"e dönüşür. Bunun yerine İ/I/ı hepsi "i" yapılır, sonra küçük harfe çevrilir.
*/
export function normalizeUsername(value: string): string {
return value.trim().replace(/[İIı]/g, "i").toLowerCase();
}
export const usernameSchema = z
.string()
.trim()
.transform((s) => s.toLocaleLowerCase("tr-TR"))
.transform(normalizeUsername)
.pipe(z.string().regex(USERNAME_PATTERN, v.usernameFormat));
export const emailSchema = z.string().trim().toLowerCase().pipe(z.email(v.email));
+8 -1
View File
@@ -12,11 +12,18 @@ const base = {
};
describe("usernameSchema", () => {
it("küçük harfe çevirir ve boşlukları kırpar (Türkçe İ dahil)", () => {
it("küçük harfe çevirir ve boşlukları kırpar", () => {
expect(usernameSchema.parse(" AYSE.K ")).toBe("ayse.k");
expect(usernameSchema.parse("Ali_2")).toBe("ali_2");
});
it("I/İ/ı harflerini i yapar (tr-TR'deki I → ı tuzağı)", () => {
expect(usernameSchema.parse("ADMIN")).toBe("admin");
expect(usernameSchema.parse("ALI")).toBe("ali");
expect(usernameSchema.parse("İREM")).toBe("irem");
expect(usernameSchema.parse("ırmak")).toBe("irmak");
});
it("Türkçe karakter, boşluk ve kısa değerleri reddeder", () => {
for (const bad of ["ayşe", "ali veli", "ab", "a".repeat(31), "ali-veli"]) {
expect(usernameSchema.safeParse(bad).success, bad).toBe(false);