Compare commits
2 Commits
be97d94ef6
...
099ec2150e
| Author | SHA1 | Date | |
|---|---|---|---|
| 099ec2150e | |||
| 9328556c28 |
@@ -29,7 +29,7 @@ Open:
|
|||||||
|
|
||||||
- App: `http://127.0.0.1:3000`
|
- App: `http://127.0.0.1:3000`
|
||||||
- Health check: `http://127.0.0.1:8080/health`
|
- Health check: `http://127.0.0.1:8080/health`
|
||||||
- Swagger UI: `http://127.0.0.1:8080/docs`
|
- Swagger UI: `http://127.0.0.1:8080/docs/`
|
||||||
- OpenAPI document: `http://127.0.0.1:8080/openapi.json`
|
- OpenAPI document: `http://127.0.0.1:8080/openapi.json`
|
||||||
|
|
||||||
After backend changes, restart it or run `docker compose up --build` again so the running container uses the latest code.
|
After backend changes, restart it or run `docker compose up --build` again so the running container uses the latest code.
|
||||||
@@ -80,6 +80,7 @@ Errors use the same `requestId` returned in the `X-Request-ID` header. See Swagg
|
|||||||
- The backend validates card numbers and refreshes ZarinHub access tokens automatically.
|
- The backend validates card numbers and refreshes ZarinHub access tokens automatically.
|
||||||
- Every conversion attempt is audited in SQLite.
|
- Every conversion attempt is audited in SQLite.
|
||||||
- Card numbers and upstream responses are sanitized before storage; credentials and tokens are not persisted.
|
- Card numbers and upstream responses are sanitized before storage; credentials and tokens are not persisted.
|
||||||
|
- Unknown frontend routes display a branded 404 page with a link back to the application.
|
||||||
- SQLite data is stored in the `card_to_iban_data` Docker volume when using Compose.
|
- SQLite data is stored in the `card_to_iban_data` Docker volume when using Compose.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ const swaggerUI = `<!doctype html>
|
|||||||
// RegisterDocumentation registers the OpenAPI document and its Swagger UI.
|
// RegisterDocumentation registers the OpenAPI document and its Swagger UI.
|
||||||
func RegisterDocumentation(mux *http.ServeMux) {
|
func RegisterDocumentation(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("GET /openapi.json", serveOpenAPI)
|
mux.HandleFunc("GET /openapi.json", serveOpenAPI)
|
||||||
mux.HandleFunc("GET /swagger/", serveSwaggerUI)
|
mux.HandleFunc("GET /docs/", serveSwaggerUI)
|
||||||
mux.HandleFunc("GET /docs", redirectToSwaggerUI)
|
mux.HandleFunc("GET /docs", redirectToDocumentation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveOpenAPI(w http.ResponseWriter, _ *http.Request) {
|
func serveOpenAPI(w http.ResponseWriter, _ *http.Request) {
|
||||||
@@ -48,6 +48,6 @@ func serveSwaggerUI(w http.ResponseWriter, _ *http.Request) {
|
|||||||
_, _ = w.Write([]byte(swaggerUI))
|
_, _ = w.Write([]byte(swaggerUI))
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectToSwaggerUI(w http.ResponseWriter, r *http.Request) {
|
func redirectToDocumentation(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/swagger/", http.StatusTemporaryRedirect)
|
http.Redirect(w, r, "/docs/", http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,21 @@ func TestRegisterDocumentation(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Swagger UI",
|
name: "Swagger UI",
|
||||||
path: "/swagger/",
|
path: "/docs/",
|
||||||
wantStatus: http.StatusOK,
|
wantStatus: http.StatusOK,
|
||||||
wantContentType: "text/html; charset=utf-8",
|
wantContentType: "text/html; charset=utf-8",
|
||||||
wantBody: `url: "/openapi.json"`,
|
wantBody: `url: "/openapi.json"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "documentation shortcut",
|
name: "documentation trailing slash",
|
||||||
path: "/docs",
|
path: "/docs",
|
||||||
wantStatus: http.StatusTemporaryRedirect,
|
wantStatus: http.StatusTemporaryRedirect,
|
||||||
wantLocation: "/swagger/",
|
wantLocation: "/docs/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "removed Swagger route",
|
||||||
|
path: "/swagger/",
|
||||||
|
wantStatus: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,3 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-dvh">
|
<NuxtPage />
|
||||||
<header class="border-border bg-surface h-14 border-b shadow-[0_1px_2px_rgba(0,0,0,.03)]">
|
|
||||||
<div class="mx-auto flex h-full w-full max-w-[1120px] items-center px-4 sm:px-6 lg:px-8">
|
|
||||||
<BrandLogo />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="mx-auto w-full max-w-[760px] px-4 py-10 sm:px-6 sm:py-14">
|
|
||||||
<section aria-labelledby="page-title">
|
|
||||||
<p class="text-brand mb-2 text-sm font-bold">سرویس تبدیل کارت به شبا</p>
|
|
||||||
<h1 id="page-title" class="text-foreground text-2xl leading-[1.5] font-extrabold sm:text-3xl">
|
|
||||||
شماره شبا را سریع و مطمئن پیدا کنید
|
|
||||||
</h1>
|
|
||||||
<p class="text-muted mt-3 max-w-[620px] text-sm leading-8 sm:text-[15px]">
|
|
||||||
استعلام مستقیماً از سرویس زرینهاب انجام میشود. شماره کامل کارت در گزارشها یا پاسخ عمومی نگهداری نمیشود.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="mt-7 sm:mt-8">
|
|
||||||
<CardConverter :api-base-url="config.public.apiBaseUrl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-muted mt-5 flex items-start gap-2 text-xs leading-6">
|
|
||||||
<svg viewBox="0 0 24 24" class="mt-0.5 size-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
|
||||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />
|
|
||||||
<path d="m9 12 2 2 4-4" />
|
|
||||||
</svg>
|
|
||||||
برای حفاظت از اطلاعات بانکی، فقط نسخه پوشیدهشده شماره کارت در سوابق فنی ثبت میشود.
|
|
||||||
</p>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
21
frontend/app/error.test.ts
Normal file
21
frontend/app/error.test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import ErrorPage from './error.vue'
|
||||||
|
|
||||||
|
describe('ErrorPage', () => {
|
||||||
|
it('renders a branded not-found page with a way home', () => {
|
||||||
|
const wrapper = mount(ErrorPage, {
|
||||||
|
props: {
|
||||||
|
error: {
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Page not found',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.get('h1').text()).toBe('صفحه پیدا نشد')
|
||||||
|
expect(wrapper.get('a').attributes('href')).toBe('/')
|
||||||
|
expect(wrapper.get('[role="img"]').attributes('aria-label')).toBe('زرینپال')
|
||||||
|
})
|
||||||
|
})
|
||||||
55
frontend/app/error.vue
Normal file
55
frontend/app/error.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NuxtError } from '#app'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import BrandLogo from './components/BrandLogo.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
error: NuxtError
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isNotFound = computed(() => props.error.statusCode === 404)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-dvh">
|
||||||
|
<header class="border-border bg-surface h-14 border-b shadow-[0_1px_2px_rgba(0,0,0,.03)]">
|
||||||
|
<div class="mx-auto flex h-full w-full max-w-[1120px] items-center px-4 sm:px-6 lg:px-8">
|
||||||
|
<BrandLogo />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="mx-auto flex w-full max-w-[760px] items-center px-4 py-12 sm:min-h-[calc(100dvh-3.5rem)] sm:px-6 sm:py-16">
|
||||||
|
<section
|
||||||
|
class="card border-border bg-surface w-full border p-6 sm:p-8"
|
||||||
|
aria-labelledby="error-title"
|
||||||
|
>
|
||||||
|
<div class="bg-accent text-brand mb-5 grid size-12 place-items-center rounded-lg" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" class="size-6" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M12 8v5m0 3.5v.01" stroke-linecap="round" />
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-brand mb-2 text-sm font-bold">خطای {{ error.statusCode }}</p>
|
||||||
|
<h1 id="error-title" class="text-foreground text-2xl leading-[1.5] font-extrabold sm:text-3xl">
|
||||||
|
{{ isNotFound ? 'صفحه پیدا نشد' : 'خطایی رخ داده است' }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted mt-3 max-w-[560px] text-sm leading-8 sm:text-[15px]">
|
||||||
|
{{
|
||||||
|
isNotFound
|
||||||
|
? 'نشانی واردشده معتبر نیست یا صفحه موردنظر جابهجا شده است.'
|
||||||
|
: 'در نمایش این صفحه مشکلی پیش آمد. لطفاً دوباره تلاش کنید.'
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="bg-brand hover:bg-brand-hover text-on-brand mt-6 inline-flex min-h-11 items-center justify-center rounded-lg px-5 text-sm font-bold transition-colors"
|
||||||
|
>
|
||||||
|
بازگشت به صفحه اصلی
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
37
frontend/app/pages/index.vue
Normal file
37
frontend/app/pages/index.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-dvh">
|
||||||
|
<header class="border-border bg-surface h-14 border-b shadow-[0_1px_2px_rgba(0,0,0,.03)]">
|
||||||
|
<div class="mx-auto flex h-full w-full max-w-[1120px] items-center px-4 sm:px-6 lg:px-8">
|
||||||
|
<BrandLogo />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="mx-auto w-full max-w-[760px] px-4 py-10 sm:px-6 sm:py-14">
|
||||||
|
<section aria-labelledby="page-title">
|
||||||
|
<p class="text-brand mb-2 text-sm font-bold">سرویس تبدیل کارت به شبا</p>
|
||||||
|
<h1 id="page-title" class="text-foreground text-2xl leading-[1.5] font-extrabold sm:text-3xl">
|
||||||
|
شماره شبا را سریع و مطمئن پیدا کنید
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted mt-3 max-w-[620px] text-sm leading-8 sm:text-[15px]">
|
||||||
|
استعلام مستقیماً از سرویس زرینهاب انجام میشود. شماره کامل کارت در گزارشها یا پاسخ عمومی نگهداری نمیشود.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="mt-7 sm:mt-8">
|
||||||
|
<CardConverter :api-base-url="config.public.apiBaseUrl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted mt-5 flex items-start gap-2 text-xs leading-6">
|
||||||
|
<svg viewBox="0 0 24 24" class="mt-0.5 size-4 shrink-0" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />
|
||||||
|
<path d="m9 12 2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
برای حفاظت از اطلاعات بانکی، فقط نسخه پوشیدهشده شماره کارت در سوابق فنی ثبت میشود.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user