Fix admin settings save 400 and remove JSON socials editor.
Send only whitelisted fields and let operators edit social links as simple label/url rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,47 +24,106 @@
|
|||||||
<input v-model="form.address" class="input" />
|
<input v-model="form.address" class="input" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Timezone</label>
|
<label class="label">منطقه زمانی</label>
|
||||||
<input v-model="form.timezone" class="input" dir="ltr" />
|
<input v-model="form.timezone" class="input" dir="ltr" placeholder="Asia/Tehran" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field" style="grid-column: 1 / -1">
|
||||||
<label class="label">لوگو URL</label>
|
<label class="label">آدرس لوگو</label>
|
||||||
<input v-model="form.logoUrl" class="input" dir="ltr" />
|
<input v-model="form.logoUrl" class="input" dir="ltr" placeholder="از بخش رسانه آپلود کنید و آدرس را اینجا بگذارید" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
|
||||||
<label class="label">شبکههای اجتماعی (JSON)</label>
|
<h2 style="margin: 1.25rem 0 0.75rem; font-size: 1rem">شبکههای اجتماعی</h2>
|
||||||
<textarea v-model="socialsText" class="textarea" dir="ltr" />
|
<div v-for="(s, i) in socials" :key="i" class="item-box">
|
||||||
|
<div class="row" style="justify-content: space-between; margin-bottom: 0.5rem">
|
||||||
|
<strong>مورد {{ i + 1 }}</strong>
|
||||||
|
<button type="button" class="btn danger" @click="socials.splice(i, 1)">حذف</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">نام (مثلاً لینکدین)</label>
|
||||||
|
<input v-model="s.label" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">آدرس لینک</label>
|
||||||
|
<input v-model="s.url" class="input" dir="ltr" placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="msg" class="muted">{{ msg }}</p>
|
<button type="button" class="btn ghost" @click="socials.push({ label: '', url: '' })">+ افزودن شبکه اجتماعی</button>
|
||||||
<button class="btn">ذخیره</button>
|
|
||||||
|
<p v-if="msg" class="muted" style="margin-top: 1rem">{{ msg }}</p>
|
||||||
|
<button class="btn" style="margin-top: 0.75rem">ذخیره</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
|
|
||||||
const form = ref<any>(null);
|
const form = ref<{
|
||||||
const socialsText = ref("[]");
|
brandName: string;
|
||||||
|
tagline: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
address: string;
|
||||||
|
timezone: string;
|
||||||
|
logoUrl: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const socials = reactive<Array<{ label: string; url: string }>>([]);
|
||||||
const msg = ref("");
|
const msg = ref("");
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const { data } = await api.get("/admin/settings");
|
const { data } = await api.get("/admin/settings");
|
||||||
form.value = data;
|
form.value = {
|
||||||
socialsText.value = JSON.stringify(data.socials ?? [], null, 2);
|
brandName: data.brandName || "",
|
||||||
|
tagline: data.tagline || "",
|
||||||
|
email: data.email || "",
|
||||||
|
phone: data.phone || "",
|
||||||
|
address: data.address || "",
|
||||||
|
timezone: data.timezone || "Asia/Tehran",
|
||||||
|
logoUrl: data.logoUrl || "",
|
||||||
|
};
|
||||||
|
socials.splice(0, socials.length, ...(Array.isArray(data.socials) ? data.socials : []));
|
||||||
|
if (!socials.length) socials.push({ label: "", url: "" });
|
||||||
});
|
});
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (!form.value) return;
|
||||||
msg.value = "";
|
msg.value = "";
|
||||||
try {
|
try {
|
||||||
form.value.socials = JSON.parse(socialsText.value);
|
const payload = {
|
||||||
const { data } = await api.put("/admin/settings", form.value);
|
brandName: form.value.brandName,
|
||||||
form.value = data;
|
tagline: form.value.tagline,
|
||||||
|
email: form.value.email,
|
||||||
|
phone: form.value.phone,
|
||||||
|
address: form.value.address,
|
||||||
|
timezone: form.value.timezone,
|
||||||
|
logoUrl: form.value.logoUrl || null,
|
||||||
|
socials: socials.filter((s) => s.label.trim() || s.url.trim()),
|
||||||
|
};
|
||||||
|
const { data } = await api.put("/admin/settings", payload);
|
||||||
|
form.value.logoUrl = data.logoUrl || "";
|
||||||
msg.value = "ذخیره شد.";
|
msg.value = "ذخیره شد.";
|
||||||
} catch {
|
} catch (e: any) {
|
||||||
msg.value = "خطا در ذخیره یا JSON نامعتبر.";
|
const detail = e?.response?.data?.message;
|
||||||
|
msg.value = Array.isArray(detail)
|
||||||
|
? `خطا: ${detail.join("، ")}`
|
||||||
|
: detail
|
||||||
|
? `خطا: ${detail}`
|
||||||
|
: "خطا در ذخیره.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.item-box {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.9rem;
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
background: #12151b;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { Body, Controller, Get, Param, Put, UseGuards } from "@nestjs/common";
|
import { Body, Controller, Get, Param, Put, UseGuards } from "@nestjs/common";
|
||||||
import { ContentService } from "./content.service";
|
import { ContentService } from "./content.service";
|
||||||
import { JwtAuthGuard } from "../auth/jwt-auth.guard";
|
import { JwtAuthGuard } from "../auth/jwt-auth.guard";
|
||||||
import { IsObject, IsOptional, IsString } from "class-validator";
|
import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
|
||||||
|
class SocialDto {
|
||||||
|
@IsString()
|
||||||
|
label!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
url!: string;
|
||||||
|
}
|
||||||
|
|
||||||
class UpdateSettingsDto {
|
class UpdateSettingsDto {
|
||||||
@IsOptional() @IsString() brandName?: string;
|
@IsOptional() @IsString() brandName?: string;
|
||||||
@@ -10,8 +19,12 @@ class UpdateSettingsDto {
|
|||||||
@IsOptional() @IsString() phone?: string;
|
@IsOptional() @IsString() phone?: string;
|
||||||
@IsOptional() @IsString() address?: string;
|
@IsOptional() @IsString() address?: string;
|
||||||
@IsOptional() @IsString() timezone?: string;
|
@IsOptional() @IsString() timezone?: string;
|
||||||
@IsOptional() socials?: object;
|
@IsOptional() @IsString() logoUrl?: string | null;
|
||||||
@IsOptional() @IsString() logoUrl?: string;
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => SocialDto)
|
||||||
|
socials?: SocialDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpsertSectionDto {
|
class UpsertSectionDto {
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ export class ContentService {
|
|||||||
socials?: object;
|
socials?: object;
|
||||||
logoUrl?: string | null;
|
logoUrl?: string | null;
|
||||||
}) {
|
}) {
|
||||||
return this.prisma.siteSettings.update({ where: { id: "default" }, data });
|
const { logoUrl, ...rest } = data;
|
||||||
|
return this.prisma.siteSettings.update({
|
||||||
|
where: { id: "default" },
|
||||||
|
data: {
|
||||||
|
...rest,
|
||||||
|
...(logoUrl !== undefined ? { logoUrl: logoUrl || null } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async listSections() {
|
async listSections() {
|
||||||
|
|||||||
Reference in New Issue
Block a user