feat(shared): zod şemaları, Türkçe arama ve TL/tarih biçimleri

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 01:35:10 +03:00
parent c875847b65
commit 4dbd2eb441
11 changed files with 504 additions and 3 deletions
+10
View File
@@ -0,0 +1,10 @@
import { z } from "zod";
import { tr } from "../../i18n/tr";
/** Giriş formu: kullanıcı adı veya e-posta + şifre. Biçim kontrolü sunucuda yapılır. */
export const loginSchema = z.object({
identifier: z.string().trim().min(1, tr.auth.identifierRequired),
password: z.string().min(1, tr.auth.passwordRequired),
});
export type LoginInput = z.infer<typeof loginSchema>;
+36
View File
@@ -0,0 +1,36 @@
import { z } from "zod";
import { tr } from "../../i18n/tr";
const v = tr.validation;
/** Sistem rolleri (Better Auth admin plugin). */
export const SYSTEM_ROLES = ["admin", "user"] as const;
export const systemRoleSchema = z.enum(SYSTEM_ROLES);
export type SystemRole = z.infer<typeof systemRoleSchema>;
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. */
export const usernameSchema = z
.string()
.trim()
.transform((s) => s.toLocaleLowerCase("tr-TR"))
.pipe(z.string().regex(USERNAME_PATTERN, v.usernameFormat));
export const emailSchema = z.string().trim().toLowerCase().pipe(z.email(v.email));
/** Boş bırakılabilen e-posta: "" → undefined. */
export const optionalEmailSchema = z
.string()
.trim()
.transform((s) => (s === "" ? undefined : s.toLowerCase()))
.pipe(z.email(v.email).optional());
export const passwordSchema = z.string().min(8, v.passwordMin).max(128, v.passwordMax);
/** Girilen değer e-posta mı (giriş sırasında e-posta/kullanıcı adı yöntemini seçmek için)? */
export function looksLikeEmail(value: string): boolean {
return value.includes("@");
}
+73
View File
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { loginSchema } from "./auth";
import { looksLikeEmail, usernameSchema } from "./common";
import { banUserSchema, userCreateSchema, userUpdateSchema } from "./user";
const base = {
name: "Ayşe Yılmaz",
username: "ayse",
email: "",
role: "user",
password: "gizli-1234",
};
describe("usernameSchema", () => {
it("küçük harfe çevirir ve boşlukları kırpar (Türkçe İ dahil)", () => {
expect(usernameSchema.parse(" AYSE.K ")).toBe("ayse.k");
expect(usernameSchema.parse("Ali_2")).toBe("ali_2");
});
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);
}
});
});
describe("userCreateSchema", () => {
it("boş e-postayı undefined yapar", () => {
const out = userCreateSchema.parse(base);
expect(out.email).toBeUndefined();
expect(out.name).toBe("Ayşe Yılmaz");
});
it("e-postayı küçük harfe çevirir ve doğrular", () => {
expect(userCreateSchema.parse({ ...base, email: " Ayse@Ornek.COM " }).email).toBe(
"ayse@ornek.com",
);
expect(userCreateSchema.safeParse({ ...base, email: "gecersiz" }).success).toBe(false);
});
it("kısa şifreyi ve geçersiz rolü reddeder", () => {
expect(userCreateSchema.safeParse({ ...base, password: "1234567" }).success).toBe(false);
expect(userCreateSchema.safeParse({ ...base, role: "owner" }).success).toBe(false);
});
it("güncelleme şemasında şifre yoktur", () => {
const { password: _password, ...rest } = base;
expect(userUpdateSchema.parse(rest)).not.toHaveProperty("password");
});
});
describe("banUserSchema", () => {
it("boş nedeni undefined yapar, uzun nedeni reddeder", () => {
expect(banUserSchema.parse({ reason: " " }).reason).toBeUndefined();
expect(banUserSchema.safeParse({ reason: "x".repeat(201) }).success).toBe(false);
});
});
describe("loginSchema", () => {
it("boş alanları Türkçe mesajla reddeder", () => {
const r = loginSchema.safeParse({ identifier: " ", password: "" });
expect(r.success).toBe(false);
expect(r.error?.issues.map((i) => i.message)).toEqual([
"Kullanıcı adı veya e-posta girin",
"Şifre girin",
]);
});
it("e-posta / kullanıcı adı ayrımı", () => {
expect(looksLikeEmail("ayse@ornek.com")).toBe(true);
expect(looksLikeEmail("ayse")).toBe(false);
});
});
+39
View File
@@ -0,0 +1,39 @@
import { z } from "zod";
import { tr } from "../../i18n/tr";
import {
nameSchema,
optionalEmailSchema,
passwordSchema,
systemRoleSchema,
usernameSchema,
} from "./common";
/** Admin: yeni kullanıcı. E-posta isteğe bağlı (çocuklar vb. için kullanıcı adıyla giriş). */
export const userCreateSchema = z.object({
name: nameSchema,
username: usernameSchema,
email: optionalEmailSchema,
role: systemRoleSchema,
password: passwordSchema,
});
/** Admin: kullanıcı bilgisi güncelleme (şifre ayrı akışta). */
export const userUpdateSchema = userCreateSchema.omit({ password: true });
export const setPasswordSchema = z.object({ password: passwordSchema });
export const banUserSchema = z.object({
reason: z
.string()
.trim()
.max(200, tr.validation.banReasonMax)
.transform((s) => (s === "" ? undefined : s))
.optional(),
});
export type UserCreateInput = z.input<typeof userCreateSchema>;
export type UserCreate = z.output<typeof userCreateSchema>;
export type UserUpdateInput = z.input<typeof userUpdateSchema>;
export type UserUpdate = z.output<typeof userUpdateSchema>;
export type SetPasswordInput = z.input<typeof setPasswordSchema>;
export type BanUserInput = z.input<typeof banUserSchema>;