fix the errors in dashboard
This commit is contained in:
@@ -11,6 +11,10 @@ DASHBOARD_URL=http://localhost:3000/dashboard
|
||||
ADMIN_URL=http://localhost:3000/admin
|
||||
DASHBOARD_INTERNAL_URL=http://host.docker.internal:3001
|
||||
ADMIN_INTERNAL_URL=http://host.docker.internal:3002
|
||||
# Asset prefix for the dashboard/admin dev servers so browsers load JS chunks from the
|
||||
# correct port rather than through the marketplace proxy (which doesn't have those chunks).
|
||||
DASHBOARD_ASSET_PREFIX=http://localhost:3001/dashboard
|
||||
ADMIN_ASSET_PREFIX=http://localhost:3002/admin
|
||||
JWT_SECRET=dev-secret
|
||||
JWT_EXPIRY=8h
|
||||
RENTER_JWT_EXPIRY=7d
|
||||
|
||||
@@ -157,6 +157,9 @@ deploy_to_vps:
|
||||
echo '-- Pulling pre-built image --'
|
||||
docker pull $DOCKER_IMAGE
|
||||
|
||||
echo '-- Ensuring uploads volume exists --'
|
||||
docker volume create rentaldrivego-prod_api_uploads >/dev/null
|
||||
|
||||
echo '-- Restarting services --'
|
||||
APP_IMAGE=$IMAGE_REPOSITORY \
|
||||
APP_VERSION=$CI_COMMIT_SHORT_SHA \
|
||||
|
||||
@@ -235,6 +235,8 @@ docker compose -p rentaldrivego-portainer-prod --env-file .env.docker.production
|
||||
|
||||
Uploaded assets handled by the API, including vehicle photos and company branding images, are persisted in the named Docker volume `api_uploads`, mounted inside the API container at `/var/lib/rentaldrivego/storage`. Rebuilding or redeploying the API no longer clears those files as long as that volume is kept.
|
||||
|
||||
The production helper scripts and the GitLab deploy job now explicitly create the Docker volume `${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}_api_uploads` before starting or redeploying the API stack, so a fresh server bootstrap does not accidentally start the API without its upload storage volume.
|
||||
|
||||
#### Updating after a code change
|
||||
|
||||
Pull the latest code and rebuild only the changed service:
|
||||
|
||||
+13
-1
@@ -78,6 +78,15 @@ export function createApp() {
|
||||
res.status(404).end()
|
||||
})
|
||||
|
||||
// Public storage assets (logos, vehicle photos, etc.) must be loadable cross-origin
|
||||
// by the browser. Without this header, helmet's default CORP: same-origin reaches
|
||||
// 404 responses (when express.static calls next() on a miss) and the browser blocks
|
||||
// the response even for broken-image cases, producing ERR_BLOCKED_BY_RESPONSE.
|
||||
app.use('/storage', (_req, res, next) => {
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
|
||||
next()
|
||||
})
|
||||
|
||||
// Serve uploaded assets from the configured storage root, with a legacy fallback
|
||||
// for older files that were written under the previous API-local path.
|
||||
app.use('/storage', express.static(getStorageRoot()))
|
||||
@@ -88,7 +97,10 @@ export function createApp() {
|
||||
// Webhook must use raw body BEFORE express.json()
|
||||
app.use('/api/v1/webhooks', express.raw({ type: 'application/json' }), webhookRouter)
|
||||
|
||||
app.use(helmet())
|
||||
// Let /storage responses manage CORP explicitly so missing files still return
|
||||
// a normal cross-origin 404 instead of being blocked by Helmet's default
|
||||
// same-origin policy.
|
||||
app.use(helmet({ crossOriginResourcePolicy: false }))
|
||||
if (process.env.NODE_ENV !== 'test') app.use(morgan('combined'))
|
||||
app.use(express.json({ limit: '10mb' }))
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ router.post('/auth/login', async (req, res, next) => {
|
||||
if (!result) return res.status(401).json({ error: 'invalid_credentials', message: 'Invalid email or password', statusCode: 401 })
|
||||
if ('totpRequired' in result) return res.status(401).json({ error: 'totp_required', message: '2FA code required', statusCode: 401 })
|
||||
if ('invalidTotp' in result) return res.status(401).json({ error: 'invalid_totp', message: 'Invalid 2FA code', statusCode: 401 })
|
||||
ok(res, { data: result })
|
||||
ok(res, result)
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ router.post('/auth/forgot-password', async (req, res, next) => {
|
||||
try {
|
||||
const { email } = parseBody(forgotPasswordSchema, req)
|
||||
await service.forgotPassword(email)
|
||||
ok(res, { data: { message: 'If that email is registered, a reset link has been sent.' } })
|
||||
ok(res, { message: 'If that email is registered, a reset link has been sent.' })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -40,19 +40,19 @@ router.post('/auth/reset-password', async (req, res, next) => {
|
||||
const { token, password } = parseBody(resetPasswordSchema, req)
|
||||
const ok2 = await service.resetPassword(token, password)
|
||||
if (!ok2) return res.status(400).json({ error: 'invalid_token', message: 'Reset link is invalid or has expired.', statusCode: 400 })
|
||||
ok(res, { data: { message: 'Password updated successfully. You can now sign in.' } })
|
||||
ok(res, { message: 'Password updated successfully. You can now sign in.' })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ─── Auth (protected) ──────────────────────────────────────────
|
||||
|
||||
router.get('/auth/me', requireAdminAuth, (req, res) => {
|
||||
ok(res, { data: presentAdminUser(req.admin as any) })
|
||||
ok(res, presentAdminUser(req.admin as any))
|
||||
})
|
||||
|
||||
router.post('/auth/2fa/setup', requireAdminAuth, async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.setupTotp(req.admin.id, req.admin.email) })
|
||||
ok(res, await service.setupTotp(req.admin.id, req.admin.email))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ router.post('/auth/2fa/verify', requireAdminAuth, async (req, res, next) => {
|
||||
const { code } = parseBody(totpVerifySchema, req)
|
||||
const valid = await service.verifyTotp(req.admin.id, code)
|
||||
if (!valid) return res.status(400).json({ error: 'invalid_code', message: 'Invalid 2FA code', statusCode: 400 })
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -76,7 +76,7 @@ router.get('/companies', requireAdminAuth, async (req, res, next) => {
|
||||
router.get('/companies/:id', requireAdminAuth, async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
ok(res, { data: await service.getCompany(id) })
|
||||
ok(res, await service.getCompany(id))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -84,7 +84,7 @@ router.patch('/companies/:id', requireAdminAuth, requireAdminRole('SUPPORT'), as
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
const body = parseBody(adminCompanyUpdateSchema, req)
|
||||
ok(res, { data: await service.updateCompany(id, body, req.admin.id, req.ip) })
|
||||
ok(res, await service.updateCompany(id, body, req.admin.id, req.ip))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -92,7 +92,7 @@ router.patch('/companies/:id/status', requireAdminAuth, requireAdminRole('SUPPOR
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
const { status, reason } = parseBody(companyStatusSchema, req)
|
||||
ok(res, { data: await service.setCompanyStatus(id, status, reason, req.admin.id, req.ip) })
|
||||
ok(res, await service.setCompanyStatus(id, status, reason, req.admin.id, req.ip))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -100,14 +100,14 @@ router.delete('/companies/:id', requireAdminAuth, requireAdminRole('ADMIN'), asy
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
await service.deleteCompany(id, req.admin.id, req.ip)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/companies/:id/impersonate', requireAdminAuth, requireAdminRole('ADMIN'), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
ok(res, { data: await service.impersonateCompany(id, req.admin.id, req.ip) })
|
||||
ok(res, await service.impersonateCompany(id, req.admin.id, req.ip))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -123,7 +123,7 @@ router.post('/renters/:id/block', requireAdminAuth, requireAdminRole('SUPPORT'),
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
await service.setRenterActive(id, false)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -131,7 +131,7 @@ router.post('/renters/:id/unblock', requireAdminAuth, requireAdminRole('SUPPORT'
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
await service.setRenterActive(id, true)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -139,7 +139,7 @@ router.post('/renters/:id/unblock', requireAdminAuth, requireAdminRole('SUPPORT'
|
||||
|
||||
router.get('/metrics', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getPlatformMetrics() })
|
||||
ok(res, await service.getPlatformMetrics())
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -155,7 +155,7 @@ router.get('/audit-logs', requireAdminAuth, requireAdminRole('ADMIN'), async (re
|
||||
|
||||
router.get('/admins', requireAdminAuth, requireAdminRole('SUPER_ADMIN'), async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.listAdmins() })
|
||||
ok(res, await service.listAdmins())
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -170,7 +170,7 @@ router.patch('/admins/:id/role', requireAdminAuth, requireAdminRole('SUPER_ADMIN
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
const { role } = parseBody(adminRoleSchema, req)
|
||||
await service.updateAdminRole(id, role)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -178,7 +178,7 @@ router.patch('/admins/:id/permissions', requireAdminAuth, requireAdminRole('SUPE
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
const { permissions } = parseBody(adminPermissionsSchema, req)
|
||||
ok(res, { data: await service.updateAdminPermissions(id, permissions) })
|
||||
ok(res, await service.updateAdminPermissions(id, permissions))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -212,14 +212,14 @@ router.get('/billing/invoices/:invoiceId/pdf', requireAdminAuth, requireAdminRol
|
||||
|
||||
router.get('/site-config/marketplace-homepage', requireAdminAuth, async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getMarketplaceHomepage() })
|
||||
ok(res, await service.getMarketplaceHomepage())
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.patch('/site-config/marketplace-homepage', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => {
|
||||
try {
|
||||
const { homepage } = parseBody(homepageUpdateSchema, req)
|
||||
ok(res, { data: await service.updateMarketplaceHomepage(homepage, req.admin.id, req.ip) })
|
||||
ok(res, await service.updateMarketplaceHomepage(homepage, req.admin.id, req.ip))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
|
||||
@@ -15,19 +15,19 @@ router.use(requireCompanyAuth, requireTenant, requireSubscription)
|
||||
router.get('/summary', async (req, res, next) => {
|
||||
try {
|
||||
const { period } = parseQuery(summaryQuerySchema, req)
|
||||
ok(res, { data: await service.getSummary(req.companyId, period) })
|
||||
ok(res, await service.getSummary(req.companyId, period))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/dashboard', async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getDashboard(req.companyId) })
|
||||
ok(res, await service.getDashboard(req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/sources', async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getSources(req.companyId) })
|
||||
ok(res, await service.getSources(req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -42,7 +42,7 @@ router.get('/report', requireRole('MANAGER'), async (req, res, next) => {
|
||||
res.setHeader('Content-Disposition', `attachment; filename="report-${start}-${end}.csv"`)
|
||||
return res.send(result.csv)
|
||||
}
|
||||
ok(res, { data: result.report })
|
||||
ok(res, result.report)
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
|
||||
@@ -1,27 +1,91 @@
|
||||
import { prisma } from '../../lib/prisma'
|
||||
|
||||
let customerLicenseImageColumnSupported: boolean | null = null
|
||||
|
||||
async function hasCustomerLicenseImageColumn() {
|
||||
if (customerLicenseImageColumnSupported !== null) {
|
||||
return customerLicenseImageColumnSupported
|
||||
}
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe<Array<{ exists: boolean }>>(
|
||||
"select exists(select 1 from information_schema.columns where table_schema = 'public' and table_name = 'customers' and column_name = 'licenseImageUrl') as exists"
|
||||
)
|
||||
customerLicenseImageColumnSupported = Boolean(rows[0]?.exists)
|
||||
return customerLicenseImageColumnSupported
|
||||
}
|
||||
|
||||
function buildCustomerSelect(includeLicenseImageUrl: boolean) {
|
||||
return {
|
||||
id: true,
|
||||
companyId: true,
|
||||
renterId: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
driverLicense: true,
|
||||
dateOfBirth: true,
|
||||
nationality: true,
|
||||
address: true,
|
||||
notes: true,
|
||||
flagged: true,
|
||||
flagReason: true,
|
||||
licenseExpiry: true,
|
||||
licenseIssuedAt: true,
|
||||
licenseCountry: true,
|
||||
licenseNumber: true,
|
||||
licenseCategory: true,
|
||||
licenseExpired: true,
|
||||
licenseExpiringSoon: true,
|
||||
licenseValidationStatus: true,
|
||||
licenseApprovedBy: true,
|
||||
licenseApprovedAt: true,
|
||||
licenseApprovalNote: true,
|
||||
...(includeLicenseImageUrl ? { licenseImageUrl: true } : {}),
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}
|
||||
}
|
||||
|
||||
async function getCustomerSelect() {
|
||||
return buildCustomerSelect(await hasCustomerLicenseImageColumn())
|
||||
}
|
||||
|
||||
async function sanitizeCustomerData(data: Record<string, unknown>) {
|
||||
if (await hasCustomerLicenseImageColumn()) {
|
||||
return data
|
||||
}
|
||||
|
||||
const { licenseImageUrl, ...rest } = data
|
||||
void licenseImageUrl
|
||||
return rest as Record<string, unknown>
|
||||
}
|
||||
|
||||
export async function findMany(where: any, skip: number, take: number) {
|
||||
const select = await getCustomerSelect()
|
||||
return Promise.all([
|
||||
prisma.customer.findMany({ where, skip, take, orderBy: { createdAt: 'desc' } }),
|
||||
prisma.customer.findMany({ where, skip, take, orderBy: { createdAt: 'desc' }, select }),
|
||||
prisma.customer.count({ where }),
|
||||
])
|
||||
}
|
||||
|
||||
export async function findById(id: string, companyId: string) {
|
||||
const select = await getCustomerSelect()
|
||||
return prisma.customer.findFirst({
|
||||
where: { id, companyId },
|
||||
include: {
|
||||
select: {
|
||||
...select,
|
||||
reservations: { include: { vehicle: true }, orderBy: { createdAt: 'desc' }, take: 20 },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function findByIdSimple(id: string, companyId: string) {
|
||||
return prisma.customer.findFirst({ where: { id, companyId } })
|
||||
return prisma.customer.findFirst({ where: { id, companyId }, select: await getCustomerSelect() })
|
||||
}
|
||||
|
||||
export async function create(data: any) {
|
||||
return prisma.customer.create({ data })
|
||||
return prisma.customer.create({ data: (await sanitizeCustomerData(data)) as any, select: await getCustomerSelect() })
|
||||
}
|
||||
|
||||
export async function updateMany(id: string, companyId: string, data: any) {
|
||||
@@ -29,9 +93,13 @@ export async function updateMany(id: string, companyId: string, data: any) {
|
||||
}
|
||||
|
||||
export async function updateById(id: string, data: any) {
|
||||
return prisma.customer.update({ where: { id }, data })
|
||||
return prisma.customer.update({
|
||||
where: { id },
|
||||
data: (await sanitizeCustomerData(data)) as any,
|
||||
select: await getCustomerSelect(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function findUniqueOrThrow(id: string) {
|
||||
return prisma.customer.findUniqueOrThrow({ where: { id } })
|
||||
return prisma.customer.findUniqueOrThrow({ where: { id }, select: await getCustomerSelect() })
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ const companyAuth = [requireCompanyAuth, requireTenant, requireSubscription] as
|
||||
router.get('/company', ...companyAuth, async (req, res, next) => {
|
||||
try {
|
||||
const { unread } = parseQuery(unreadQuerySchema, req)
|
||||
ok(res, { data: await service.listCompany(req.companyId, unread) })
|
||||
ok(res, await service.listCompany(req.companyId, unread))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/unread-count', ...companyAuth, async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: { unread: await service.countUnread(req.companyId) } })
|
||||
ok(res, { unread: await service.countUnread(req.companyId) })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -31,20 +31,20 @@ router.post('/company/:id/read', ...companyAuth, async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
await service.markRead(id, req.companyId)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/company/read-all', ...companyAuth, async (req, res, next) => {
|
||||
try {
|
||||
await service.markAllRead(req.companyId)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/company/preferences', ...companyAuth, async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getPreferences(req.employee.id) })
|
||||
ok(res, await service.getPreferences(req.employee.id))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -52,7 +52,7 @@ router.patch('/company/preferences', ...companyAuth, async (req, res, next) => {
|
||||
try {
|
||||
const prefs = parseBody(preferencesSchema, req)
|
||||
await service.setPreferences(req.employee.id, prefs)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -60,7 +60,7 @@ router.patch('/company/preferences', ...companyAuth, async (req, res, next) => {
|
||||
|
||||
router.get('/renter', requireRenterAuth, async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.listRenter(req.renterId) })
|
||||
ok(res, await service.listRenter(req.renterId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -68,20 +68,20 @@ router.post('/renter/:id/read', requireRenterAuth, async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
await service.markRenterRead(id, req.renterId)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/renter/read-all', requireRenterAuth, async (req, res, next) => {
|
||||
try {
|
||||
await service.markAllRenterRead(req.renterId)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/renter/preferences', requireRenterAuth, async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getRenterPreferences(req.renterId) })
|
||||
ok(res, await service.getRenterPreferences(req.renterId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -89,7 +89,7 @@ router.patch('/renter/preferences', requireRenterAuth, async (req, res, next) =>
|
||||
try {
|
||||
const prefs = parseBody(preferencesSchema, req)
|
||||
await service.setRenterPreferences(req.renterId, prefs)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
|
||||
@@ -15,21 +15,21 @@ router.use(requireCompanyAuth, requireTenant, requireSubscription)
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const query = parseQuery(listQuerySchema, req)
|
||||
ok(res, { data: await service.listOffers(req.companyId, query) })
|
||||
ok(res, await service.listOffers(req.companyId, query))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/', requireRole('MANAGER'), async (req, res, next) => {
|
||||
try {
|
||||
const { vehicleIds, ...body } = parseBody(offerSchema, req)
|
||||
created(res, { data: await service.createOffer(req.companyId, body, vehicleIds) })
|
||||
created(res, await service.createOffer(req.companyId, body, vehicleIds))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
ok(res, { data: await service.getOffer(id, req.companyId) })
|
||||
ok(res, await service.getOffer(id, req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ router.patch('/:id', requireRole('MANAGER'), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
const { vehicleIds, ...body } = parseBody(offerSchema.partial(), req)
|
||||
ok(res, { data: await service.updateOffer(id, req.companyId, body, vehicleIds) })
|
||||
ok(res, await service.updateOffer(id, req.companyId, body, vehicleIds))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ router.delete('/:id', requireRole('MANAGER'), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
await service.deleteOffer(id, req.companyId)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ router.post('/:id/activate', requireRole('MANAGER'), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
await service.setOfferActive(id, req.companyId, true)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -61,14 +61,14 @@ router.post('/:id/deactivate', requireRole('MANAGER'), async (req, res, next) =>
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
await service.setOfferActive(id, req.companyId, false)
|
||||
ok(res, { data: { success: true } })
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/:id/stats', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
ok(res, { data: await service.getOfferStats(id, req.companyId) })
|
||||
ok(res, await service.getOfferStats(id, req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
|
||||
@@ -42,14 +42,14 @@ router.use(requireCompanyAuth, requireTenant, requireSubscription)
|
||||
|
||||
router.get('/company', async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.listByCompany(req.companyId) })
|
||||
ok(res, await service.listByCompany(req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/reservations/:id', async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(reservationParamSchema, req)
|
||||
ok(res, { data: await service.listByReservation(id, req.companyId) })
|
||||
ok(res, await service.listByReservation(id, req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ router.post('/reservations/:id/charge', requireRole('MANAGER'), async (req, res,
|
||||
try {
|
||||
const { id } = parseParams(reservationParamSchema, req)
|
||||
const body = parseBody(chargeSchema, req)
|
||||
ok(res, { data: await service.initCharge(id, req.companyId, body) })
|
||||
ok(res, await service.initCharge(id, req.companyId, body))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -65,7 +65,7 @@ router.post('/reservations/:id/capture-paypal', requireRole('MANAGER'), async (r
|
||||
try {
|
||||
const { id } = parseParams(reservationParamSchema, req)
|
||||
const { paypalOrderId } = parseBody(capturePaypalSchema, req)
|
||||
ok(res, { data: await service.capturePaypal(id, req.companyId, paypalOrderId) })
|
||||
ok(res, await service.capturePaypal(id, req.companyId, paypalOrderId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -73,7 +73,7 @@ router.post('/reservations/:id/manual', requireRole('MANAGER'), async (req, res,
|
||||
try {
|
||||
const { id } = parseParams(reservationParamSchema, req)
|
||||
const body = parseBody(manualPaymentSchema, req)
|
||||
ok(res, { data: await service.recordManualPayment(id, req.companyId, body) })
|
||||
ok(res, await service.recordManualPayment(id, req.companyId, body))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -81,7 +81,7 @@ router.post('/reservations/:reservationId/payments/:paymentId/refund', requireRo
|
||||
try {
|
||||
const { reservationId, paymentId } = parseParams(paymentParamSchema, req)
|
||||
const { amount, reason } = parseBody(refundSchema, req)
|
||||
ok(res, { data: await service.refundPayment(reservationId, paymentId, req.companyId, amount, reason) })
|
||||
ok(res, await service.refundPayment(reservationId, paymentId, req.companyId, amount, reason))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@ const router = Router()
|
||||
// ─── Public ────────────────────────────────────────────────────
|
||||
|
||||
router.get('/plans', (_req, res) => {
|
||||
ok(res, { data: service.getPlans() })
|
||||
ok(res, service.getPlans())
|
||||
})
|
||||
|
||||
router.get('/providers', (_req, res) => {
|
||||
ok(res, { data: service.getProviders() })
|
||||
ok(res, service.getProviders())
|
||||
})
|
||||
|
||||
// ─── Webhooks (no auth) ────────────────────────────────────────
|
||||
@@ -50,7 +50,7 @@ router.post('/webhooks/paypal', async (req, res, next) => {
|
||||
router.post('/capture-paypal', requireCompanyAuth, requireTenant, async (req, res, next) => {
|
||||
try {
|
||||
const { paypalOrderId } = parseBody(capturePaypalSchema, req)
|
||||
ok(res, { data: await service.capturePaypal(req.companyId, paypalOrderId) })
|
||||
ok(res, await service.capturePaypal(req.companyId, paypalOrderId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -60,39 +60,39 @@ router.use(requireCompanyAuth, requireTenant)
|
||||
|
||||
router.get('/me', async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getSubscription(req.companyId) })
|
||||
ok(res, await service.getSubscription(req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/invoices', async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getInvoices(req.companyId) })
|
||||
ok(res, await service.getInvoices(req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/checkout', requireRole('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const body = parseBody(checkoutSchema, req)
|
||||
ok(res, { data: await service.checkout(req.companyId, body) })
|
||||
ok(res, await service.checkout(req.companyId, body))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/change-plan', requireRole('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const body = parseBody(changePlanSchema, req)
|
||||
ok(res, { data: await service.changePlan(req.companyId, body) })
|
||||
ok(res, await service.changePlan(req.companyId, body))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/cancel', requireRole('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.cancel(req.companyId) })
|
||||
ok(res, await service.cancel(req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/resume', requireRole('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.resume(req.companyId) })
|
||||
ok(res, await service.resume(req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ router.use(requireCompanyAuth, requireTenant, requireSubscription)
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
ok(res, { data: await service.getMembers(req.companyId) })
|
||||
ok(res, await service.getMembers(req.companyId))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -35,28 +35,28 @@ router.patch('/:id/role', requireRole('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
const { role } = parseBody(roleSchema, req)
|
||||
ok(res, { data: await service.updateEmployeeRole(req.companyId, req.employee.id, req.employee.role, id, { role }) })
|
||||
ok(res, await service.updateEmployeeRole(req.companyId, req.employee.id, req.employee.role, id, { role }))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/:id/deactivate', requireRole('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
ok(res, { data: await service.deactivateEmployee(req.companyId, req.employee.role, id) })
|
||||
ok(res, await service.deactivateEmployee(req.companyId, req.employee.role, id))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/:id/reactivate', requireRole('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
ok(res, { data: await service.reactivateEmployee(req.companyId, req.employee.role, id) })
|
||||
ok(res, await service.reactivateEmployee(req.companyId, req.employee.role, id))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.delete('/:id', requireRole('OWNER'), async (req, res, next) => {
|
||||
try {
|
||||
const { id } = parseParams(idParamSchema, req)
|
||||
ok(res, { data: await service.removeEmployee(req.companyId, req.employee.role, id) })
|
||||
ok(res, await service.removeEmployee(req.companyId, req.employee.role, id))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
|
||||
@@ -23,8 +23,14 @@ const storagePatterns = [
|
||||
.filter(Boolean)
|
||||
.filter((p, i, arr) => arr.findIndex((q) => q.hostname === p.hostname && q.port === p.port) === i)
|
||||
|
||||
// When the dashboard is accessed via the marketplace proxy (localhost:3000/dashboard),
|
||||
// the browser resolves _next/static chunk URLs relative to localhost:3000 — but those
|
||||
// chunks only exist on the dashboard's own server (localhost:3001). DASHBOARD_ASSET_PREFIX
|
||||
// makes Next.js embed absolute chunk URLs so the browser fetches them from the right port.
|
||||
// Set in .env.docker.dev; unset for local/production (where no cross-port proxy is used).
|
||||
const nextConfig = {
|
||||
basePath: '/dashboard',
|
||||
...(process.env.DASHBOARD_ASSET_PREFIX ? { assetPrefix: process.env.DASHBOARD_ASSET_PREFIX } : {}),
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
|
||||
@@ -324,7 +324,10 @@ export default function OffersPage() {
|
||||
|
||||
function load() {
|
||||
apiFetch<Offer[]>('/offers')
|
||||
.then((data) => setOffers(data ?? []))
|
||||
.then((data) => {
|
||||
setOffers(Array.isArray(data) ? data : [])
|
||||
setError(null)
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
}
|
||||
|
||||
@@ -334,7 +337,7 @@ export default function OffersPage() {
|
||||
setLoadingFleet(true)
|
||||
try {
|
||||
const data = await apiFetch<FleetVehicle[]>('/vehicles?pageSize=200')
|
||||
setFleet(data ?? [])
|
||||
setFleet(Array.isArray(data) ? data : [])
|
||||
} catch {
|
||||
// non-fatal; vehicle picker just stays empty
|
||||
} finally {
|
||||
@@ -363,7 +366,7 @@ export default function OffersPage() {
|
||||
const detail = await apiFetch<Offer>(`/offers/${offer.id}`)
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
vehicleIds: detail.vehicles?.map((v) => v.vehicleId) ?? [],
|
||||
vehicleIds: Array.isArray(detail?.vehicles) ? detail.vehicles.map((v) => v.vehicleId) : [],
|
||||
}))
|
||||
} catch {
|
||||
// leave vehicleIds as empty if fetch fails
|
||||
|
||||
@@ -117,11 +117,19 @@ export default function SignInPageClient({ embedded = false }: { embedded?: bool
|
||||
changed = true
|
||||
}
|
||||
|
||||
// useSearchParams() can return empty params during hydration without a Suspense boundary.
|
||||
// Always preserve embedded=1 when the component was server-rendered as embedded so
|
||||
// the router.replace doesn't strip it and trigger an unintended redirect.
|
||||
if (embedded && params.get('embedded') !== '1') {
|
||||
params.set('embedded', '1')
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (!changed) return
|
||||
|
||||
const nextQuery = params.toString()
|
||||
router.replace(nextQuery ? `${pathname}?${nextQuery}` : pathname, { scroll: false })
|
||||
}, [language, pathname, router, searchParams, theme])
|
||||
}, [embedded, language, pathname, router, searchParams, theme])
|
||||
|
||||
useEffect(() => {
|
||||
const currentPath = window.location.pathname + (window.location.search || '')
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Suspense } from 'react'
|
||||
import { headers } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import SignInPageClient from './SignInPageClient'
|
||||
|
||||
const MARKETPLACE_URL = process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'
|
||||
|
||||
function isInternalHost(host: string | null): boolean {
|
||||
if (!host) return false
|
||||
const hostname = host.split(':')[0]?.toLowerCase()
|
||||
return ['host.docker.internal', 'dashboard', 'admin', 'api', 'marketplace'].includes(hostname)
|
||||
}
|
||||
|
||||
export default async function SignInPage({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -13,6 +22,7 @@ export default async function SignInPage({
|
||||
if (!embedded) {
|
||||
const requestHeaders = headers()
|
||||
const host = requestHeaders.get('host')
|
||||
const forwardedHost = requestHeaders.get('x-forwarded-host')
|
||||
const proto = requestHeaders.get('x-forwarded-proto') ?? 'http'
|
||||
const query = new URLSearchParams()
|
||||
|
||||
@@ -25,12 +35,22 @@ export default async function SignInPage({
|
||||
}
|
||||
}
|
||||
|
||||
const destination = host
|
||||
? `${proto}://${host}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
||||
: `${process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
||||
const marketplaceOrigin = new URL(MARKETPLACE_URL)
|
||||
const publicHost = forwardedHost && !isInternalHost(forwardedHost)
|
||||
? forwardedHost
|
||||
: host && !isInternalHost(host)
|
||||
? host
|
||||
: null
|
||||
|
||||
redirect(destination)
|
||||
if (!publicHost || publicHost === marketplaceOrigin.host) {
|
||||
const destination = `${MARKETPLACE_URL}/sign-in${query.size ? `?${query.toString()}` : ''}`
|
||||
redirect(destination)
|
||||
}
|
||||
}
|
||||
|
||||
return <SignInPageClient embedded={embedded} />
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<SignInPageClient embedded={embedded} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ type FooterItem = {
|
||||
href?: string
|
||||
}
|
||||
|
||||
const appPrivacyHref = {
|
||||
en: `${marketplaceUrl}/app-privacy-en`,
|
||||
fr: `${marketplaceUrl}/app-privacy-fr`,
|
||||
ar: `${marketplaceUrl}/app-privacy-ar`,
|
||||
} as const
|
||||
|
||||
const localeOptions: Array<{ value: DashboardLanguage; label: string; flag: string }> = [
|
||||
{ value: 'en', label: 'Global (English)', flag: '🇺🇸' },
|
||||
{ value: 'ar', label: 'North Africa (Arabic)', flag: '🇲🇦' },
|
||||
@@ -34,13 +40,13 @@ function getFooterContent(language: DashboardLanguage): {
|
||||
{ label: 'Conditions d’utilisation', href: `${marketplaceUrl}/footer/terms-of-service` },
|
||||
{ label: 'Sécurité', href: `${marketplaceUrl}/footer/security` },
|
||||
{ label: 'Conformité', href: `${marketplaceUrl}/footer/compliance` },
|
||||
{ label: 'Politique de confidentialité', href: `${marketplaceUrl}/footer/privacy-policy` },
|
||||
{ label: 'Politique de confidentialité', href: appPrivacyHref.fr },
|
||||
{ label: 'Politique relative aux cookies', href: `${marketplaceUrl}/footer/cookie-policy` },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'Newsletter', href: `${marketplaceUrl}/footer/newsletter` },
|
||||
{ label: 'Contacter les ventes', href: `${marketplaceUrl}/footer/contact-sales` },
|
||||
{ label: 'Nos bureaux', href: `${marketplaceUrl}/footer/our-offices` },
|
||||
{ label: 'Conditions générales', href: `${marketplaceUrl}/footer/general-conditions` },
|
||||
],
|
||||
localeLabel: 'Europe (French)',
|
||||
rightsLabel: 'Tous droits réservés.',
|
||||
@@ -52,13 +58,13 @@ function getFooterContent(language: DashboardLanguage): {
|
||||
{ label: 'شروط الاستخدام', href: `${marketplaceUrl}/footer/terms-of-service` },
|
||||
{ label: 'الأمان', href: `${marketplaceUrl}/footer/security` },
|
||||
{ label: 'الامتثال', href: `${marketplaceUrl}/footer/compliance` },
|
||||
{ label: 'سياسة الخصوصية', href: `${marketplaceUrl}/footer/privacy-policy` },
|
||||
{ label: 'سياسة الخصوصية', href: appPrivacyHref.ar },
|
||||
{ label: 'سياسة ملفات تعريف الارتباط', href: `${marketplaceUrl}/footer/cookie-policy` },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'النشرة الإخبارية', href: `${marketplaceUrl}/footer/newsletter` },
|
||||
{ label: 'تواصل مع المبيعات', href: `${marketplaceUrl}/footer/contact-sales` },
|
||||
{ label: 'مكاتبنا', href: `${marketplaceUrl}/footer/our-offices` },
|
||||
{ label: 'الشروط العامة', href: `${marketplaceUrl}/footer/general-conditions` },
|
||||
],
|
||||
localeLabel: 'North Africa (Arabic)',
|
||||
rightsLabel: 'جميع الحقوق محفوظة.',
|
||||
@@ -71,13 +77,13 @@ function getFooterContent(language: DashboardLanguage): {
|
||||
{ label: 'Terms of Service', href: `${marketplaceUrl}/footer/terms-of-service` },
|
||||
{ label: 'Security', href: `${marketplaceUrl}/footer/security` },
|
||||
{ label: 'Compliance', href: `${marketplaceUrl}/footer/compliance` },
|
||||
{ label: 'Privacy Policy', href: `${marketplaceUrl}/footer/privacy-policy` },
|
||||
{ label: 'Privacy Policy', href: appPrivacyHref.en },
|
||||
{ label: 'Cookie Policy', href: `${marketplaceUrl}/footer/cookie-policy` },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'Newsletter', href: `${marketplaceUrl}/footer/newsletter` },
|
||||
{ label: 'Contact Sales', href: `${marketplaceUrl}/footer/contact-sales` },
|
||||
{ label: 'Our Offices', href: `${marketplaceUrl}/footer/our-offices` },
|
||||
{ label: 'General Conditions', href: `${marketplaceUrl}/footer/general-conditions` },
|
||||
],
|
||||
localeLabel: 'Global (English)',
|
||||
rightsLabel: 'All rights reserved.',
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
const PUBLIC_DASHBOARD_PREFIXES = ['/dashboard/sign-in', '/dashboard/forgot-password']
|
||||
const MARKETPLACE_URL = process.env.NEXT_PUBLIC_MARKETPLACE_URL ?? 'http://localhost:3000'
|
||||
|
||||
function isInternalHost(host: string | null): boolean {
|
||||
if (!host) return false
|
||||
const hostname = host.split(':')[0]?.toLowerCase()
|
||||
return ['host.docker.internal', 'dashboard', 'admin', 'api', 'marketplace'].includes(hostname)
|
||||
}
|
||||
|
||||
function isProtectedRoute(req: NextRequest) {
|
||||
return req.nextUrl.pathname === '/dashboard' || req.nextUrl.pathname.startsWith('/dashboard/')
|
||||
const { pathname } = req.nextUrl
|
||||
if (PUBLIC_DASHBOARD_PREFIXES.some((p) => pathname === p || pathname.startsWith(p + '/'))) return false
|
||||
return pathname === '/dashboard' || pathname.startsWith('/dashboard/')
|
||||
}
|
||||
|
||||
function localJwtMiddleware(req: NextRequest): NextResponse {
|
||||
if (!isProtectedRoute(req)) return NextResponse.next()
|
||||
|
||||
// Check for employee_token cookie (set on login) OR allow if coming from sign-in
|
||||
const token = req.cookies.get('employee_token')?.value
|
||||
if (!token) {
|
||||
const forwardedHost = req.headers.get('x-forwarded-host')
|
||||
const forwardedProto = req.headers.get('x-forwarded-proto')
|
||||
const marketplaceOrigin = new URL(MARKETPLACE_URL)
|
||||
|
||||
const signInUrl = req.nextUrl.clone()
|
||||
signInUrl.pathname = '/sign-in'
|
||||
if (forwardedHost && !isInternalHost(forwardedHost)) {
|
||||
signInUrl.host = forwardedHost
|
||||
signInUrl.protocol = (forwardedProto ?? 'http') + ':'
|
||||
} else if (isInternalHost(signInUrl.host)) {
|
||||
signInUrl.host = marketplaceOrigin.host
|
||||
signInUrl.protocol = marketplaceOrigin.protocol
|
||||
}
|
||||
|
||||
const isMarketplaceHost = signInUrl.host === marketplaceOrigin.host
|
||||
signInUrl.pathname = isMarketplaceHost ? '/sign-in' : '/dashboard/sign-in'
|
||||
signInUrl.searchParams.set('redirect', req.nextUrl.pathname)
|
||||
return NextResponse.redirect(signInUrl)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import FooterContentPage from '@/components/FooterContentPage'
|
||||
|
||||
export default function AppPrivacyArPage() {
|
||||
return <FooterContentPage slug="privacy-policy" forcedLanguage="ar" />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import FooterContentPage from '@/components/FooterContentPage'
|
||||
|
||||
export default function AppPrivacyEnPage() {
|
||||
return <FooterContentPage slug="privacy-policy" forcedLanguage="en" />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import FooterContentPage from '@/components/FooterContentPage'
|
||||
|
||||
export default function AppPrivacyFrPage() {
|
||||
return <FooterContentPage slug="privacy-policy" forcedLanguage="fr" />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import FooterContentPage from '@/components/FooterContentPage'
|
||||
|
||||
export default function AppTermsArPage() {
|
||||
return <FooterContentPage slug="general-conditions" forcedLanguage="ar" />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import FooterContentPage from '@/components/FooterContentPage'
|
||||
|
||||
export default function AppTermsEnPage() {
|
||||
return <FooterContentPage slug="general-conditions" forcedLanguage="en" />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import FooterContentPage from '@/components/FooterContentPage'
|
||||
|
||||
export default function AppTermsFrPage() {
|
||||
return <FooterContentPage slug="general-conditions" forcedLanguage="fr" />
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMarketplacePreferences } from '@/components/MarketplaceShell'
|
||||
import { type FooterPageSlug, getFooterPageContent } from '@/lib/footerContent'
|
||||
import { type MarketplaceLanguage } from '@/lib/i18n'
|
||||
|
||||
const pageMeta = {
|
||||
en: {
|
||||
@@ -21,11 +22,41 @@ const pageMeta = {
|
||||
},
|
||||
} as const
|
||||
|
||||
export default function FooterContentPage({ slug }: { slug: FooterPageSlug }) {
|
||||
const urlPattern = /(https?:\/\/[^\s]+)/g
|
||||
|
||||
function renderParagraphText(paragraph: string) {
|
||||
const parts = paragraph.split(urlPattern)
|
||||
return parts.map((part, index) => {
|
||||
if (urlPattern.test(part)) {
|
||||
urlPattern.lastIndex = 0
|
||||
const match = part.match(/^(https?:\/\/[^\s]+?)([.,!?;:]*)$/)
|
||||
const href = match?.[1] ?? part
|
||||
const trailing = match?.[2] ?? ''
|
||||
|
||||
return (
|
||||
<span key={`${part}-${index}`}>
|
||||
<a
|
||||
href={href}
|
||||
className="text-amber-700 underline decoration-amber-300 underline-offset-4 transition hover:text-stone-950 dark:text-amber-300 dark:hover:text-stone-100"
|
||||
>
|
||||
{href}
|
||||
</a>
|
||||
{trailing}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
urlPattern.lastIndex = 0
|
||||
return <span key={`${part}-${index}`}>{part}</span>
|
||||
})
|
||||
}
|
||||
|
||||
export default function FooterContentPage({ slug, forcedLanguage }: { slug: FooterPageSlug; forcedLanguage?: MarketplaceLanguage }) {
|
||||
const { language } = useMarketplacePreferences()
|
||||
const content = getFooterPageContent(language, slug)
|
||||
const meta = pageMeta[language]
|
||||
const isArabic = language === 'ar'
|
||||
const activeLanguage = forcedLanguage ?? language
|
||||
const content = getFooterPageContent(activeLanguage, slug)
|
||||
const meta = pageMeta[activeLanguage]
|
||||
const isArabic = activeLanguage === 'ar'
|
||||
|
||||
return (
|
||||
<main className="site-page">
|
||||
@@ -43,10 +74,23 @@ export default function FooterContentPage({ slug }: { slug: FooterPageSlug }) {
|
||||
<div className={`space-y-6 px-6 py-10 sm:px-10 sm:py-12 ${isArabic ? 'text-right' : 'text-left'}`}>
|
||||
{content.paragraphs.map((paragraph) => (
|
||||
<p key={paragraph} className="text-lg leading-8 text-stone-700 dark:text-stone-300">
|
||||
{paragraph}
|
||||
{renderParagraphText(paragraph)}
|
||||
</p>
|
||||
))}
|
||||
|
||||
{content.sections?.map((section) => (
|
||||
<section key={section.heading} className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-stone-900 dark:text-stone-100">
|
||||
{section.heading}
|
||||
</h2>
|
||||
{section.paragraphs.map((paragraph) => (
|
||||
<p key={`${section.heading}-${paragraph}`} className="text-lg leading-8 text-stone-700 dark:text-stone-300">
|
||||
{renderParagraphText(paragraph)}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<div className="rounded-3xl border border-amber-200 bg-amber-50 px-6 py-5 dark:border-amber-800 dark:bg-amber-950/30">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.18em] text-amber-800 dark:text-amber-400">
|
||||
{meta.cta}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { footerPageHref } from '@/lib/footerContent'
|
||||
import { appPrivacyHref, footerPageHref } from '@/lib/footerContent'
|
||||
import { MARKETPLACE_LANGUAGE_COOKIE, SHARED_LANGUAGE_COOKIE, isMarketplaceLanguage, type MarketplaceLanguage } from '@/lib/i18n'
|
||||
import { SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
|
||||
|
||||
@@ -76,13 +76,13 @@ export function getFooterContent(language: MarketplaceLanguage): {
|
||||
{ label: "Conditions d'utilisation", href: footerPageHref['terms-of-service'] },
|
||||
{ label: 'Sécurité', href: footerPageHref.security },
|
||||
{ label: 'Conformité', href: footerPageHref.compliance },
|
||||
{ label: 'Politique de confidentialité', href: footerPageHref['privacy-policy'] },
|
||||
{ label: 'Politique de confidentialité', href: appPrivacyHref.fr },
|
||||
{ label: 'Politique relative aux cookies', href: footerPageHref['cookie-policy'] },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'Newsletter', href: footerPageHref.newsletter },
|
||||
{ label: 'Contacter les ventes', href: footerPageHref['contact-sales'] },
|
||||
{ label: 'Nos bureaux', href: footerPageHref['our-offices'] },
|
||||
{ label: 'Conditions générales', href: footerPageHref['general-conditions'] },
|
||||
],
|
||||
localeLabel: 'Europe (French)',
|
||||
rightsLabel: 'Tous droits réservés.',
|
||||
@@ -94,13 +94,13 @@ export function getFooterContent(language: MarketplaceLanguage): {
|
||||
{ label: 'شروط الاستخدام', href: footerPageHref['terms-of-service'] },
|
||||
{ label: 'الأمان', href: footerPageHref.security },
|
||||
{ label: 'الامتثال', href: footerPageHref.compliance },
|
||||
{ label: 'سياسة الخصوصية', href: footerPageHref['privacy-policy'] },
|
||||
{ label: 'سياسة الخصوصية', href: appPrivacyHref.ar },
|
||||
{ label: 'سياسة ملفات تعريف الارتباط', href: footerPageHref['cookie-policy'] },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'النشرة الإخبارية', href: footerPageHref.newsletter },
|
||||
{ label: 'تواصل مع المبيعات', href: footerPageHref['contact-sales'] },
|
||||
{ label: 'مكاتبنا', href: footerPageHref['our-offices'] },
|
||||
{ label: 'الشروط العامة', href: footerPageHref['general-conditions'] },
|
||||
],
|
||||
localeLabel: 'North Africa (Arabic)',
|
||||
rightsLabel: 'جميع الحقوق محفوظة.',
|
||||
@@ -113,13 +113,13 @@ export function getFooterContent(language: MarketplaceLanguage): {
|
||||
{ label: 'Terms of Service', href: footerPageHref['terms-of-service'] },
|
||||
{ label: 'Security', href: footerPageHref.security },
|
||||
{ label: 'Compliance', href: footerPageHref.compliance },
|
||||
{ label: 'Privacy Policy', href: footerPageHref['privacy-policy'] },
|
||||
{ label: 'Privacy Policy', href: appPrivacyHref.en },
|
||||
{ label: 'Cookie Policy', href: footerPageHref['cookie-policy'] },
|
||||
],
|
||||
secondary: [
|
||||
{ label: 'Newsletter', href: footerPageHref.newsletter },
|
||||
{ label: 'Contact Sales', href: footerPageHref['contact-sales'] },
|
||||
{ label: 'Our Offices', href: footerPageHref['our-offices'] },
|
||||
{ label: 'General Conditions', href: footerPageHref['general-conditions'] },
|
||||
],
|
||||
localeLabel: 'Global (English)',
|
||||
rightsLabel: 'All rights reserved.',
|
||||
|
||||
@@ -9,14 +9,30 @@ export const footerPageSlugs = [
|
||||
'cookie-policy',
|
||||
'newsletter',
|
||||
'contact-sales',
|
||||
'our-offices',
|
||||
'general-conditions',
|
||||
] as const
|
||||
|
||||
export type FooterPageSlug = (typeof footerPageSlugs)[number]
|
||||
|
||||
export const appPrivacyHref = {
|
||||
en: '/app-privacy-en',
|
||||
fr: '/app-privacy-fr',
|
||||
ar: '/app-privacy-ar',
|
||||
} as const
|
||||
|
||||
export const appTermsHref = {
|
||||
en: '/app-tc-en',
|
||||
fr: '/app-tc-fr',
|
||||
ar: '/app-tc-ar',
|
||||
} as const
|
||||
|
||||
export type FooterPageContent = {
|
||||
title: string
|
||||
paragraphs: string[]
|
||||
sections?: Array<{
|
||||
heading: string
|
||||
paragraphs: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const footerPageHref: Record<FooterPageSlug, string> = {
|
||||
@@ -28,7 +44,7 @@ export const footerPageHref: Record<FooterPageSlug, string> = {
|
||||
'cookie-policy': '/footer/cookie-policy',
|
||||
newsletter: '/footer/newsletter',
|
||||
'contact-sales': '/footer/contact-sales',
|
||||
'our-offices': '/footer/our-offices',
|
||||
'general-conditions': '/footer/general-conditions',
|
||||
}
|
||||
|
||||
const footerContent: Record<MarketplaceLanguage, Record<FooterPageSlug, FooterPageContent>> = {
|
||||
@@ -63,15 +79,112 @@ const footerContent: Record<MarketplaceLanguage, Record<FooterPageSlug, FooterPa
|
||||
'privacy-policy': {
|
||||
title: 'Privacy Policy',
|
||||
paragraphs: [
|
||||
'Your privacy matters to us. RentalDriveGo collects only the information necessary to provide and improve our services, including fleet management tools, booking systems, and customer support.',
|
||||
'We do not sell personal information to third parties. Data is securely stored and processed in accordance with applicable privacy laws and industry standards.',
|
||||
'You are currently viewing the privacy policy for the RentalDriveGo website. To consult the application privacy policy, please visit: https://www.rentaldrivego.ma/app-privacy-en',
|
||||
'RentalDriveGo respects your privacy and is committed to handling your personal data in a transparent manner. This policy explains how we collect, use, store, and share your personal information when you use our website, and it describes your rights regarding the processing of your personal data.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. Data Controller Contact Details',
|
||||
paragraphs: [
|
||||
'This privacy information applies to data processing carried out by RentalDriveGo.',
|
||||
'Telephone: +212 6 05 65 17 51',
|
||||
'Email: service-client@rentaldrivego.ma',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. Personal Data We Collect, How We Use It, and How Long We Keep It',
|
||||
paragraphs: [
|
||||
'When you browse our website for information only, without registering for a service or otherwise sending us personal data, your browser automatically sends certain technical information to our hosting provider, such as your browser type, language and version, the date and time of the request, your IP address, pages visited, referring website, volume of data transferred, and operating system.',
|
||||
'This information is temporarily stored to make the website available on your device, support internal analysis, improve the presentation of the website, and detect or prevent misuse. We do not combine this data with other data sources and we do not seek to identify you directly from it. This data is deleted as soon as it is no longer needed for these purposes.',
|
||||
'If you contact us by web form or email, we may collect your name, email address, telephone number, and any other information you choose to provide. We use this information to respond to your questions, contact you when relevant services or registrations become available, or manage participation in campaigns or competitions where applicable.',
|
||||
'We keep your personal data for as long as you maintain an account with us. If you want your personal data to be removed, you must submit a formal request, subject to any legal or contractual retention obligations that may apply.',
|
||||
'You may withdraw your consent at any time where processing is based on consent. Any processing carried out before withdrawal remains lawful.',
|
||||
'We may also use personal data for website review and improvement, market research, analysis, surveys, and marketing activities related to our products and services, subject to applicable law.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. Third-Party Service Providers',
|
||||
paragraphs: [
|
||||
'We do not transfer your personal data to third parties unless we are legally required to do so, the transfer is necessary for the performance of a contractual relationship, or you have expressly agreed to it in advance.',
|
||||
'We may work with service providers in Morocco and abroad for technical and website-related services, including hosting and maintenance.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. Security',
|
||||
paragraphs: [
|
||||
'The security of your personal data is important to us. We apply appropriate organizational, technical, and physical measures to protect the personal information we collect, both during transmission and after receipt.',
|
||||
'Our security measures and privacy practices are reviewed and improved on an ongoing basis.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. Retention of Personal Data',
|
||||
paragraphs: [
|
||||
'We keep personal data only for as long as we are legally entitled to do so, as long as the purpose of processing continues, and provided that no valid objection has been raised.',
|
||||
'Where legal retention periods apply, the data will be stored for the required duration. Once the applicable period has expired, the data will be deleted unless it is still needed for the performance or preparation of a contract.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. Transfer to a Foreign Country',
|
||||
paragraphs: [
|
||||
'The arrangements described above may result in your personal data being stored or processed in foreign countries.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '7. Your Rights',
|
||||
paragraphs: [
|
||||
'Under Law No. 09-08 relating to the protection of individuals with regard to the processing of personal data, you may have the right to access your personal data, obtain additional information, request correction of inaccurate data, and object, where applicable, to the processing of your data or to its transfer to third parties.',
|
||||
'To exercise your rights, you may send a request including your full name and a copy of your identity document to: service-client@rentaldrivego.ma',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'cookie-policy': {
|
||||
title: 'Cookie Policy',
|
||||
paragraphs: [
|
||||
'RentalDriveGo uses cookies and similar technologies to improve website functionality, analyze traffic, remember user preferences, and enhance the overall user experience.',
|
||||
'By continuing to use our website, you consent to the use of cookies in accordance with this policy.',
|
||||
'We use cookies exclusively to operate the platform and remember your personal preferences. We do not use cookies for advertising, cross-site tracking, or behavioural profiling, and we do not share cookie data with third parties for marketing purposes.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. Authentication Cookie',
|
||||
paragraphs: [
|
||||
'Name: employee_token',
|
||||
'This cookie is set when you sign in to the RentalDriveGo workspace. It stores a cryptographically signed token that verifies your identity for the duration of your session. It expires automatically after 8 hours and is deleted when you sign out. This cookie is strictly necessary — without it, access to the dashboard and protected areas is not possible.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. Language Preference Cookie',
|
||||
paragraphs: [
|
||||
'Name: rentaldrivego-language',
|
||||
'Stores your chosen display language (English, French, or Arabic) so the interface appears in your preferred language on every visit across all parts of the platform. It is retained for one year.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. Theme Preference Cookie',
|
||||
paragraphs: [
|
||||
'Name: rentaldrivego-theme',
|
||||
'Stores your preferred colour scheme (light or dark mode). It is read immediately when a page loads to apply the correct theme before the interface is drawn, preventing a visual flash. It is retained for one year.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. Per-User Preference Cookies',
|
||||
paragraphs: [
|
||||
'Names: rentaldrivego-language--[id], rentaldrivego-theme--[id]',
|
||||
'When you are signed in, your language and theme preferences are also saved under a cookie linked to your account identifier. This allows multiple employees sharing a device to each maintain independent preferences. These cookies are retained for one year.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. Legacy Preference Cookies',
|
||||
paragraphs: [
|
||||
'Names: dashboard-language, marketplace-language',
|
||||
'These cookies exist for backward compatibility with earlier versions of the platform and store the same language preference as the primary cookie. They are read only as a fallback and will be phased out in a future update.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. Managing Your Cookies',
|
||||
paragraphs: [
|
||||
'You can manage or delete cookies at any time through your browser settings. Blocking the authentication cookie will prevent you from signing in. Blocking preference cookies will cause your language and theme settings to reset on each visit. For general information about cookies, you may visit http://www.allaboutcookies.org/.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
newsletter: {
|
||||
@@ -88,11 +201,93 @@ const footerContent: Record<MarketplaceLanguage, Record<FooterPageSlug, FooterPa
|
||||
'Contact us to schedule a demo, discuss pricing, or learn more about our fleet management and reservation platform.',
|
||||
],
|
||||
},
|
||||
'our-offices': {
|
||||
title: 'Our Offices',
|
||||
'general-conditions': {
|
||||
title: 'General Conditions of Use',
|
||||
paragraphs: [
|
||||
'RentalDriveGo serves customers globally through a distributed team and partner network dedicated to supporting modern car rental businesses.',
|
||||
'For business inquiries, partnerships, or support requests, please contact our team through our official channels.',
|
||||
'You are currently viewing the General Conditions of Use for the RentalDriveGo website. To consult the General Conditions of Use of the application, please visit: https://www.rentaldrivego.ma/app-tc-en',
|
||||
'These General Conditions of Use define the rules that apply when you visit, access, browse, contact us through, or otherwise interact with the website www.rentaldrivego.ma.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. Purpose',
|
||||
paragraphs: [
|
||||
'These conditions govern the use of the website www.rentaldrivego.ma, operated by RentalDriveGo, a limited liability company registered in the Azrou Trade Register under number 00000, with its registered office in Azrou',
|
||||
'They also define the standards relating to the content of the website and apply to any person who visits the website, contacts us or other users through it, links to it, or interacts with it in any way.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. Website Offer',
|
||||
paragraphs: [
|
||||
'We operate the website solely for information, prospecting, and market research purposes.',
|
||||
'Information relating to the RentalDriveGo loyalty program may be presented to visitors, and download links may be made available through the website.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. Contact',
|
||||
paragraphs: [
|
||||
'You may contact us through the website or by email at service-client@rentaldrivego.ma.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. Acceptance of the General Conditions of Use',
|
||||
paragraphs: [
|
||||
'By using the website, you accept these General Conditions of Use and agree to comply with them. If you do not accept them, you must not use the website.',
|
||||
'Our Privacy Policy and Cookie Policy also apply to your use of the website.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. Changes to the General Conditions of Use',
|
||||
paragraphs: [
|
||||
'We may amend these General Conditions of Use at any time. Each time you use the website, you should review the current version.',
|
||||
'We also reserve the right to amend, correct, or update any information published on the website at any time.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. Changes to Website Content and Availability',
|
||||
paragraphs: [
|
||||
'We may update or modify the content of the website at any time to reflect changes in the needs of users and partners.',
|
||||
'The website is made available free of charge. While we use reasonable means to prevent and resolve interruptions, we cannot guarantee continuous or uninterrupted availability.',
|
||||
'We may suspend, withdraw, or restrict the availability of all or part of the website for commercial or operational reasons.',
|
||||
'You are responsible for ensuring that all persons accessing the website through your internet connection are aware of these conditions and comply with them.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '7. Scope of Information on the Website',
|
||||
paragraphs: [
|
||||
'The content of the website is provided for general information purposes only and does not constitute a recommendation on which you should rely.',
|
||||
'Although we make reasonable efforts to update the information on the website, we do not guarantee, whether expressly or implicitly, that the content is accurate, complete, or current.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '8. Third-Party Websites',
|
||||
paragraphs: [
|
||||
'We are not responsible for third-party websites to which the website may link.',
|
||||
'Any hyperlinks to third-party content, websites, or resources are provided for information purposes only, and we cannot be held responsible for their content, accuracy, or quality.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '9. Limitation of Liability',
|
||||
paragraphs: [
|
||||
'You agree that access to and use of the website and any associated pages is at your own risk.',
|
||||
'Nothing in these conditions excludes or limits our liability where such exclusion or limitation would be unlawful.',
|
||||
'Neither we nor our partners or affiliates may be held liable for damage resulting from the use of information, files, links, or websites accessible from or received through the website.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '10. Your Responsibilities',
|
||||
paragraphs: [
|
||||
'You acknowledge that we may use the information you provide to us where you have given the required consent or where another lawful basis applies.',
|
||||
'You agree to use the website only for lawful purposes.',
|
||||
'If we consider that a breach of these General Conditions of Use has occurred, we may take any action we deem appropriate.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '11. Intellectual Property',
|
||||
paragraphs: [
|
||||
'The website is our property and the entire website is protected by intellectual property laws.',
|
||||
'All rights of reproduction and representation are reserved.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -128,18 +323,115 @@ const footerContent: Record<MarketplaceLanguage, Record<FooterPageSlug, FooterPa
|
||||
'privacy-policy': {
|
||||
title: 'Politique de confidentialité',
|
||||
paragraphs: [
|
||||
'Votre confidentialité est importante pour nous. RentalDriveGo collecte uniquement les informations nécessaires au fonctionnement et à l’amélioration de ses services.',
|
||||
'Nous ne vendons pas vos données personnelles à des tiers. Les données sont stockées et traitées de manière sécurisée conformément aux réglementations en vigueur.',
|
||||
'Vous consultez actuellement la politique de confidentialité du site RentalDriveGo. Pour consulter la politique de confidentialité de l’application, veuillez visiter : https://www.rentaldrivego.ma/app-privacy-fr',
|
||||
'RentalDriveGo respecte votre vie privée et s’engage à traiter vos données personnelles de manière transparente. Cette politique explique comment nous collectons, utilisons, conservons et partageons vos informations personnelles lorsque vous utilisez notre site internet, ainsi que les droits dont vous disposez concernant ce traitement.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. Coordonnées du responsable du traitement',
|
||||
paragraphs: [
|
||||
'Les présentes informations sur la protection des données s’appliquent aux traitements effectués par RentalDriveGo.',
|
||||
'Téléphone : +212 6 05 65 17 51',
|
||||
'Email : service-client@rentaldrivego.ma',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. Données personnelles collectées, finalités d’utilisation et durée de conservation',
|
||||
paragraphs: [
|
||||
'Lorsque vous consultez notre site à des fins d’information uniquement, sans vous inscrire à un service et sans nous transmettre de données personnelles par un autre moyen, votre navigateur envoie automatiquement certaines informations techniques à notre hébergeur, notamment le type, la langue et la version du navigateur, la date et l’heure de la demande, l’adresse IP, les pages consultées, le site d’origine, le volume de données transférées et le système d’exploitation.',
|
||||
'Ces informations sont temporairement conservées afin de permettre l’affichage du site sur votre appareil, de réaliser des analyses internes, d’améliorer la présentation du site et de détecter ou prévenir les abus. Nous ne croisons pas ces données avec d’autres sources et nous ne cherchons pas à vous identifier directement à partir de celles-ci. Elles sont supprimées dès qu’elles ne sont plus nécessaires à ces finalités.',
|
||||
'Si vous nous contactez via un formulaire ou par email, nous pouvons collecter votre nom, votre adresse email, votre numéro de téléphone et toute autre information que vous choisissez de nous communiquer. Nous utilisons ces données pour répondre à vos questions, vous contacter lorsque certains services ou inscriptions deviennent disponibles, ou gérer votre participation à des campagnes ou concours le cas échéant.',
|
||||
'Nous conservons vos données personnelles aussi longtemps que vous détenez un compte chez nous. Si vous souhaitez la suppression de vos données personnelles, vous devez nous adresser une demande formelle, sous réserve de toute obligation légale ou contractuelle de conservation applicable.',
|
||||
'Vous pouvez retirer votre consentement à tout moment lorsque le traitement repose sur celui-ci. Les traitements effectués avant ce retrait demeurent licites.',
|
||||
'Nous pouvons également utiliser les données personnelles pour l’évaluation et l’amélioration du site, les études de marché, les analyses, les enquêtes ainsi que les actions de marketing liées à nos produits et services, dans le respect de la loi applicable.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. Prestataires tiers',
|
||||
paragraphs: [
|
||||
'Nous ne communiquons pas vos données personnelles à des tiers sauf en cas d’obligation légale, lorsque ce transfert est nécessaire à l’exécution d’une relation contractuelle, ou lorsque vous y avez expressément consenti au préalable.',
|
||||
'Nous pouvons travailler avec des prestataires situés au Maroc et à l’étranger pour des services techniques et liés au site internet, notamment l’hébergement et la maintenance.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. Sécurité',
|
||||
paragraphs: [
|
||||
'La sécurité de vos données personnelles est importante pour nous. Nous mettons en place des mesures organisationnelles, techniques et physiques appropriées afin de protéger les informations personnelles collectées, aussi bien pendant leur transmission qu’après leur réception.',
|
||||
'Nos mesures de sécurité et nos pratiques de confidentialité sont revues et améliorées de manière continue.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. Conservation des données personnelles',
|
||||
paragraphs: [
|
||||
'Nous conservons les données personnelles uniquement aussi longtemps que la loi nous y autorise, tant que la finalité du traitement subsiste et en l’absence d’opposition valable.',
|
||||
'Lorsque des délais légaux de conservation s’appliquent, les données sont conservées pendant la durée requise. À l’expiration de cette période, elles sont supprimées sauf si elles restent nécessaires à l’exécution ou à la préparation d’un contrat.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. Transfert vers un pays étranger',
|
||||
paragraphs: [
|
||||
'Les traitements décrits ci-dessus peuvent entraîner le stockage ou le traitement de vos données personnelles dans des pays étrangers.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '7. Vos droits',
|
||||
paragraphs: [
|
||||
'Conformément à la loi n° 09-08 relative à la protection des personnes physiques à l’égard du traitement des données à caractère personnel, vous pouvez disposer d’un droit d’accès à vos données, d’un droit d’obtenir des informations complémentaires, d’un droit de rectification des données inexactes et, le cas échéant, d’un droit d’opposition au traitement ou au transfert de vos données à des tiers.',
|
||||
'Pour exercer vos droits, vous pouvez envoyer une demande indiquant votre nom complet et accompagnée d’une copie de votre pièce d’identité à : service-client@rentaldrivego.ma',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'cookie-policy': {
|
||||
title: 'Politique relative aux cookies',
|
||||
paragraphs: [
|
||||
'RentalDriveGo utilise des cookies et des technologies similaires pour améliorer l’expérience utilisateur, analyser le trafic du site et mémoriser vos préférences.',
|
||||
'En continuant à utiliser notre site web, vous acceptez notre utilisation des cookies.',
|
||||
'Nous utilisons des cookies exclusivement pour assurer le fonctionnement de la plateforme et mémoriser vos préférences personnelles. Nous n’utilisons pas de cookies à des fins publicitaires, de suivi intersites ou de profilage comportemental, et nous ne partageons pas les données des cookies avec des tiers à des fins marketing.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. Cookie d’authentification',
|
||||
paragraphs: [
|
||||
'Nom : employee_token',
|
||||
'Ce cookie est défini lorsque vous vous connectez à l’espace de travail RentalDriveGo. Il stocke un jeton cryptographiquement signé qui vérifie votre identité pour la durée de votre session. Il expire automatiquement après 8 heures et est supprimé lors de la déconnexion. Ce cookie est strictement nécessaire — sans lui, l’accès au tableau de bord et aux espaces protégés n’est pas possible.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. Cookie de préférence de langue',
|
||||
paragraphs: [
|
||||
'Nom : rentaldrivego-language',
|
||||
'Enregistre la langue d’affichage choisie (français, anglais ou arabe) afin que l’interface s’affiche dans votre langue préférée à chaque visite sur l’ensemble de la plateforme. Il est conservé pendant un an.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. Cookie de préférence de thème',
|
||||
paragraphs: [
|
||||
'Nom : rentaldrivego-theme',
|
||||
'Enregistre le schéma de couleurs préféré (mode clair ou sombre). Il est lu dès le chargement de la page afin d’appliquer le bon thème avant l’affichage de l’interface, évitant ainsi un changement visuel brusque. Il est conservé pendant un an.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. Cookies de préférence par utilisateur',
|
||||
paragraphs: [
|
||||
'Noms : rentaldrivego-language--[id], rentaldrivego-theme--[id]',
|
||||
'Lorsque vous êtes connecté, vos préférences de langue et de thème sont également enregistrées dans un cookie lié à votre identifiant de compte. Cela permet à plusieurs employés partageant un appareil de conserver des préférences indépendantes. Ces cookies sont conservés pendant un an.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. Cookies de préférence hérités',
|
||||
paragraphs: [
|
||||
'Noms : dashboard-language, marketplace-language',
|
||||
'Ces cookies existent pour assurer la compatibilité ascendante avec les versions antérieures de la plateforme et stockent la même préférence de langue que le cookie principal. Ils ne sont lus qu’en secours et seront supprimés dans une prochaine mise à jour.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. Gestion de vos cookies',
|
||||
paragraphs: [
|
||||
'Vous pouvez gérer ou supprimer les cookies à tout moment via les paramètres de votre navigateur. Bloquer le cookie d’authentification vous empêchera de vous connecter. Bloquer les cookies de préférence entraînera la réinitialisation de vos paramètres de langue et de thème à chaque visite. Pour des informations générales sur les cookies, vous pouvez consulter http://www.allaboutcookies.org/.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
newsletter: {
|
||||
newsletter: {
|
||||
title: 'Newsletter',
|
||||
paragraphs: [
|
||||
'Recevez les dernières actualités, mises à jour produits, conseils de gestion de flotte et nouveautés de RentalDriveGo.',
|
||||
@@ -153,11 +445,93 @@ const footerContent: Record<MarketplaceLanguage, Record<FooterPageSlug, FooterPa
|
||||
'Notre équipe commerciale est disponible pour organiser une démonstration, répondre à vos questions et vous proposer la solution adaptée à votre entreprise.',
|
||||
],
|
||||
},
|
||||
'our-offices': {
|
||||
title: 'Nos bureaux',
|
||||
'general-conditions': {
|
||||
title: 'Conditions générales d’utilisation',
|
||||
paragraphs: [
|
||||
'RentalDriveGo accompagne des entreprises de location de voitures à travers le monde grâce à une équipe distribuée et un réseau de partenaires spécialisés.',
|
||||
'Pour toute demande commerciale ou partenariat, veuillez contacter notre équipe via nos canaux officiels.',
|
||||
'Vous consultez actuellement les Conditions générales d’utilisation du site RentalDriveGo. Pour consulter les Conditions générales d’utilisation de l’application, veuillez visiter : https://www.rentaldrivego.ma/app-tc-fr',
|
||||
'Les présentes Conditions générales d’utilisation définissent les règles applicables lorsque vous visitez, accédez, parcourez, contactez ou utilisez de toute autre manière le site www.rentaldrivego.ma.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. Objet',
|
||||
paragraphs: [
|
||||
'Les présentes conditions régissent l’utilisation du site www.rentaldrivego.ma exploité par RentalDriveGo, société à responsabilité limitée immatriculée au registre du commerce d’Azrou sous le numéro 00000, dont le siège social est situé à Azrou.',
|
||||
'Elles définissent également les règles relatives au contenu du site et s’appliquent à toute personne qui visite le site, nous contacte ou contacte d’autres utilisateurs par son intermédiaire, y crée un lien ou interagit avec lui de quelque manière que ce soit.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. Offre du site',
|
||||
paragraphs: [
|
||||
'Nous exploitons le site exclusivement à des fins d’information, de prospection et d’étude de marché.',
|
||||
'Des informations relatives au programme de fidélité RentalDriveGo peuvent être portées à la connaissance des visiteurs et des liens de téléchargement peuvent être mis à leur disposition.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. Contact',
|
||||
paragraphs: [
|
||||
'Vous pouvez nous contacter via le site ou par email à l’adresse service-client@rentaldrivego.ma.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. Acceptation des Conditions générales d’utilisation',
|
||||
paragraphs: [
|
||||
'En utilisant le site, vous acceptez les présentes Conditions générales d’utilisation et vous vous engagez à les respecter. Si vous ne les acceptez pas, vous ne devez pas utiliser le site.',
|
||||
'Notre Politique de confidentialité et notre Politique relative aux cookies s’appliquent également à votre utilisation du site.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. Modification des Conditions générales d’utilisation',
|
||||
paragraphs: [
|
||||
'Nous pouvons modifier à tout moment les présentes Conditions générales d’utilisation. À chaque utilisation du site, nous vous invitons à consulter la version en vigueur.',
|
||||
'Nous nous réservons également le droit de modifier, corriger ou mettre à jour à tout moment les informations publiées sur le site.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. Modification du contenu et disponibilité du site',
|
||||
paragraphs: [
|
||||
'Nous pouvons mettre à jour ou modifier le contenu du site à tout moment afin de refléter l’évolution des besoins de nos utilisateurs et partenaires.',
|
||||
'Le site est mis à votre disposition gratuitement. Bien que nous mettions en œuvre des moyens raisonnables pour prévenir et résoudre les interruptions, nous ne pouvons garantir une disponibilité constante et ininterrompue.',
|
||||
'Nous pouvons suspendre, retirer ou restreindre l’accès à tout ou partie du site pour des raisons commerciales ou opérationnelles.',
|
||||
'Il vous appartient de veiller à ce que toute personne accédant au site par votre connexion internet ait connaissance des présentes conditions et les respecte.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '7. Portée des informations figurant sur le site',
|
||||
paragraphs: [
|
||||
'Le contenu du site est fourni à titre d’information générale uniquement et ne constitue ni un conseil ni une recommandation sur lesquels vous devriez vous appuyer.',
|
||||
'Bien que nous fassions des efforts raisonnables pour mettre à jour les informations publiées sur le site, nous ne garantissons pas, expressément ou implicitement, qu’elles soient exactes, complètes ou à jour.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '8. Responsabilité à l’égard des sites tiers',
|
||||
paragraphs: [
|
||||
'Nous ne sommes pas responsables des sites internet tiers vers lesquels le site peut contenir des liens.',
|
||||
'Les liens hypertextes vers des contenus, sites ou ressources de tiers sont fournis uniquement à titre informatif, et nous ne pouvons être tenus responsables de leur contenu, de leur exactitude ou de leur qualité.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '9. Limitation de responsabilité',
|
||||
paragraphs: [
|
||||
'Vous reconnaissez que l’accès et l’utilisation du site ainsi que des pages qui y sont rattachées se font à vos propres risques.',
|
||||
'Aucune disposition des présentes conditions n’exclut ou ne limite notre responsabilité lorsqu’une telle exclusion ou limitation serait contraire à la loi.',
|
||||
'Ni nous, ni nos partenaires ou affiliés, ne pourrons être tenus responsables des dommages résultant de l’utilisation des informations, fichiers, liens ou sites accessibles depuis le site ou reçus par son intermédiaire.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '10. Vos responsabilités',
|
||||
paragraphs: [
|
||||
'Vous reconnaissez que nous pouvons utiliser les informations que vous nous fournissez lorsque vous avez donné le consentement requis ou lorsqu’une autre base légale s’applique.',
|
||||
'Vous vous engagez à utiliser le site uniquement à des fins licites.',
|
||||
'Si nous estimons qu’une violation des présentes Conditions générales d’utilisation a eu lieu, nous pouvons prendre toute mesure que nous jugeons appropriée.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '11. Propriété intellectuelle',
|
||||
paragraphs: [
|
||||
'Le site est notre propriété et l’ensemble du site est protégé par la législation relative à la propriété intellectuelle.',
|
||||
'Tous les droits de reproduction et de représentation sont réservés.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -193,15 +567,112 @@ const footerContent: Record<MarketplaceLanguage, Record<FooterPageSlug, FooterPa
|
||||
'privacy-policy': {
|
||||
title: 'سياسة الخصوصية',
|
||||
paragraphs: [
|
||||
'خصوصيتك مهمة بالنسبة لنا. تقوم RentalDriveGo بجمع المعلومات الضرورية فقط لتقديم خدماتها وتحسين تجربة المستخدم.',
|
||||
'نحن لا نقوم ببيع البيانات الشخصية لأي طرف ثالث، ويتم تخزين البيانات ومعالجتها بشكل آمن وفقاً للقوانين المعمول بها.',
|
||||
'أنت الآن تطّلع على سياسة الخصوصية الخاصة بموقع RentalDriveGo. للاطلاع على سياسة الخصوصية الخاصة بالتطبيق، يرجى زيارة: https://www.rentaldrivego.ma/app-privacy-ar',
|
||||
'تحترم RentalDriveGo خصوصيتك وتلتزم بالتعامل مع بياناتك الشخصية بشفافية. توضح هذه السياسة كيفية جمع معلوماتك الشخصية واستخدامها وتخزينها ومشاركتها عند استخدامك لموقعنا، كما تشرح حقوقك المتعلقة بمعالجة بياناتك الشخصية.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. بيانات الاتصال الخاصة بمسؤول المعالجة',
|
||||
paragraphs: [
|
||||
'تنطبق هذه المعلومات المتعلقة بحماية البيانات على المعالجة التي تقوم بها RentalDriveGo.',
|
||||
'الهاتف: +212 6 05 65 17 51',
|
||||
'البريد الإلكتروني: service-client@rentaldrivego.ma',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. ما هي البيانات الشخصية التي نجمعها، وكيف نستخدمها، ومدة الاحتفاظ بها',
|
||||
paragraphs: [
|
||||
'عند زيارتك لموقعنا لأغراض الاطلاع فقط، من دون التسجيل في أي خدمة أو تزويدنا ببيانات شخصية بأي وسيلة أخرى، يرسل متصفحك تلقائياً بعض المعلومات التقنية إلى مزود الاستضافة، مثل نوع المتصفح ولغته وإصداره، وتاريخ ووقت الطلب، وعنوان IP، والصفحات التي تمت زيارتها، والموقع المحيل، وحجم البيانات المنقولة، ونظام التشغيل.',
|
||||
'يتم الاحتفاظ بهذه المعلومات مؤقتاً لتمكين عرض الموقع على جهازك، ودعم التحليل الداخلي، وتحسين طريقة عرض الموقع، واكتشاف أو منع أي إساءة استخدام. نحن لا ندمج هذه البيانات مع مصادر أخرى ولا نسعى إلى التعرف المباشر على هويتك من خلالها. ويتم حذفها بمجرد انتفاء الحاجة إليها لهذه الأغراض.',
|
||||
'إذا تواصلت معنا عبر نموذج إلكتروني أو عبر البريد الإلكتروني، فقد نجمع اسمك وبريدك الإلكتروني ورقم هاتفك وأي معلومات أخرى تختار تقديمها لنا. نستخدم هذه البيانات للرد على استفساراتك، أو التواصل معك عندما تصبح بعض الخدمات أو التسجيلات متاحة، أو لإدارة مشاركتك في الحملات أو المسابقات عند الاقتضاء.',
|
||||
'نحتفظ ببياناتك الشخصية طوال الفترة التي يكون لديك فيها حساب لدينا. وإذا كنت ترغب في حذف بياناتك الشخصية، فيجب عليك تقديم طلب رسمي بذلك، مع مراعاة أي التزامات قانونية أو تعاقدية قد تفرض الاحتفاظ ببعض البيانات.',
|
||||
'يمكنك سحب موافقتك في أي وقت عندما تكون المعالجة قائمة على الموافقة. وتظل المعالجة التي تمت قبل سحب الموافقة مشروعة.',
|
||||
'وقد نستخدم أيضاً البيانات الشخصية لمراجعة الموقع وتحسينه، وإجراء أبحاث السوق، والتحليلات، والاستبيانات، والأنشطة التسويقية المتعلقة بمنتجاتنا وخدماتنا، وفقاً للقانون المعمول به.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. مزودو الخدمات من الأطراف الثالثة',
|
||||
paragraphs: [
|
||||
'لا نقوم بنقل بياناتك الشخصية إلى أطراف ثالثة إلا إذا كان ذلك مطلوباً بموجب القانون، أو كان ضرورياً لتنفيذ علاقة تعاقدية، أو إذا كنت قد وافقت على ذلك صراحة مسبقاً.',
|
||||
'قد نتعاون مع مزودي خدمات في المغرب وخارجه لتقديم الخدمات التقنية والخدمات المرتبطة بالموقع، بما في ذلك الاستضافة والصيانة.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. الأمان',
|
||||
paragraphs: [
|
||||
'تمثل حماية بياناتك الشخصية أولوية بالنسبة لنا. لذلك نعتمد تدابير تنظيمية وتقنية ومادية مناسبة لحماية المعلومات الشخصية التي نجمعها، سواء أثناء نقلها أو بعد استلامها.',
|
||||
'نقوم بمراجعة وتحسين إجراءات الأمان وممارسات الخصوصية لدينا بشكل مستمر.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. الاحتفاظ بالبيانات الشخصية',
|
||||
paragraphs: [
|
||||
'نحتفظ بالبيانات الشخصية فقط للمدة التي يسمح بها القانون، وطالما أن الغرض من المعالجة لا يزال قائماً، وما لم يتم تقديم اعتراض مشروع.',
|
||||
'وعندما تكون هناك مدد قانونية للاحتفاظ بالبيانات، فإننا نلتزم بها. وبعد انتهاء المدة المعمول بها، يتم حذف البيانات ما لم تعد هناك حاجة إليها لتنفيذ عقد أو لاتخاذ خطوات تمهيدية لإبرام عقد.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. النقل إلى دولة أجنبية',
|
||||
paragraphs: [
|
||||
'قد تؤدي الترتيبات المذكورة أعلاه إلى تخزين بياناتك الشخصية أو معالجتها في دول أجنبية.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '7. حقوقك',
|
||||
paragraphs: [
|
||||
'وفقاً للقانون رقم 09-08 المتعلق بحماية الأشخاص تجاه معالجة المعطيات ذات الطابع الشخصي، قد يكون لك الحق في الوصول إلى بياناتك، والحصول على معلومات إضافية، وطلب تصحيح البيانات غير الدقيقة، والاعتراض عند الاقتضاء على معالجة بياناتك أو على نقلها إلى أطراف ثالثة.',
|
||||
'لممارسة حقوقك، يمكنك إرسال طلب يتضمن اسمك الكامل ونسخة من وثيقة هويتك إلى: service-client@rentaldrivego.ma',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'cookie-policy': {
|
||||
title: 'سياسة ملفات تعريف الارتباط',
|
||||
paragraphs: [
|
||||
'تستخدم RentalDriveGo ملفات تعريف الارتباط وتقنيات مشابهة لتحسين أداء الموقع وتحليل الاستخدام وتخصيص تجربة المستخدم.',
|
||||
'باستمرارك في استخدام الموقع، فإنك توافق على استخدامنا لملفات تعريف الارتباط.',
|
||||
'نستخدم ملفات تعريف الارتباط حصرياً لتشغيل المنصة وتذكّر تفضيلاتك الشخصية. لا نستخدم ملفات تعريف الارتباط لأغراض إعلانية أو تتبع المستخدمين عبر المواقع أو بناء ملفات تعريف سلوكية، ولا نشارك بيانات ملفات تعريف الارتباط مع أطراف ثالثة لأغراض تسويقية.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. ملف تعريف الارتباط للمصادقة',
|
||||
paragraphs: [
|
||||
'الاسم: employee_token',
|
||||
'يُعيَّن هذا الملف عند تسجيل دخولك إلى مساحة عمل RentalDriveGo. يخزّن رمزاً موقّعاً تشفيرياً يُثبت هويتك طوال مدة الجلسة. تنتهي صلاحيته تلقائياً بعد 8 ساعات ويُحذف عند تسجيل الخروج. هذا الملف ضروري بشكل مطلق — إذ لا يمكن الوصول إلى لوحة التحكم والمناطق المحمية بدونه.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. ملف تعريف الارتباط لتفضيل اللغة',
|
||||
paragraphs: [
|
||||
'الاسم: rentaldrivego-language',
|
||||
'يحفظ لغة العرض التي اخترتها (العربية أو الفرنسية أو الإنجليزية) حتى تظهر الواجهة بلغتك المفضّلة في كل زيارة وعبر جميع أجزاء المنصة. يُحتفظ به لمدة سنة.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. ملف تعريف الارتباط لتفضيل المظهر',
|
||||
paragraphs: [
|
||||
'الاسم: rentaldrivego-theme',
|
||||
'يحفظ نظام الألوان المفضّل لديك (الوضع الفاتح أو الداكن). يُقرأ فور تحميل الصفحة لتطبيق المظهر الصحيح قبل عرض الواجهة، ما يمنع حدوث وميض بصري غير مرغوب. يُحتفظ به لمدة سنة.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. ملفات تعريف الارتباط للتفضيلات الخاصة بكل مستخدم',
|
||||
paragraphs: [
|
||||
'الأسماء: rentaldrivego-language--[id], rentaldrivego-theme--[id]',
|
||||
'عند تسجيل دخولك، تُحفظ تفضيلات اللغة والمظهر الخاصة بك أيضاً في ملف مرتبط بمعرّف حسابك. يتيح ذلك لأكثر من موظف يتشاركون جهازاً واحداً الاحتفاظ بتفضيلات مستقلة. يُحتفظ بهذه الملفات لمدة سنة.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. ملفات تعريف الارتباط الموروثة',
|
||||
paragraphs: [
|
||||
'الأسماء: dashboard-language, marketplace-language',
|
||||
'توجد هذه الملفات لضمان التوافق مع الإصدارات السابقة من المنصة، وتخزّن تفضيل اللغة ذاته الموجود في الملف الرئيسي. لا تُقرأ إلا كخيار احتياطي، وستُزال في تحديث مستقبلي.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. إدارة ملفات تعريف الارتباط',
|
||||
paragraphs: [
|
||||
'يمكنك إدارة ملفات تعريف الارتباط أو حذفها في أي وقت من خلال إعدادات متصفحك. سيمنعك حظر ملف المصادقة من تسجيل الدخول. أما حظر ملفات التفضيلات فسيؤدي إلى إعادة ضبط إعدادات اللغة والمظهر في كل زيارة. للاطلاع على معلومات عامة حول ملفات تعريف الارتباط، يمكنك زيارة http://www.allaboutcookies.org/.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
newsletter: {
|
||||
@@ -218,11 +689,93 @@ const footerContent: Record<MarketplaceLanguage, Record<FooterPageSlug, FooterPa
|
||||
'فريق المبيعات لدينا جاهز لتقديم عرض توضيحي والإجابة على استفساراتك ومساعدتك في اختيار الحل المناسب لاحتياجاتك.',
|
||||
],
|
||||
},
|
||||
'our-offices': {
|
||||
title: 'مكاتبنا',
|
||||
'general-conditions': {
|
||||
title: 'الشروط العامة لاستخدام الموقع',
|
||||
paragraphs: [
|
||||
'تخدم RentalDriveGo شركات تأجير السيارات حول العالم من خلال فريق عمل وشبكة شركاء متخصصين.',
|
||||
'للاستفسارات التجارية أو طلبات الشراكة، يرجى التواصل معنا عبر القنوات الرسمية.',
|
||||
'أنت الآن تطّلع على الشروط العامة لاستخدام موقع RentalDriveGo. للاطلاع على الشروط العامة لاستخدام التطبيق، يرجى زيارة: https://www.rentaldrivego.ma/app-tc-ar',
|
||||
'تحدد هذه الشروط العامة القواعد المطبقة عند زيارتك أو دخولك أو تصفحك أو تواصلك أو تفاعلك بأي شكل آخر مع الموقع www.rentaldrivego.ma.',
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
heading: '1. الغرض',
|
||||
paragraphs: [
|
||||
'تنظم هذه الشروط استخدام الموقع www.rentaldrivego.ma الذي تديره شركة RentalDriveGo، وهي شركة ذات مسؤولية محدودة مسجلة في السجل التجاري بأزرو تحت رقم 00000، ويقع مقرها الاجتماعي في أزرو.',
|
||||
'كما تحدد هذه الشروط القواعد المتعلقة بمحتوى الموقع، وتطبق على كل شخص يزور الموقع أو يتواصل معنا أو مع مستخدمين آخرين من خلاله أو يضع رابطاً إليه أو يتفاعل معه بأي طريقة كانت.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '2. ما يتيحه الموقع',
|
||||
paragraphs: [
|
||||
'نقوم بتشغيل الموقع حصرياً لأغراض الإعلام والاستكشاف ودراسة السوق.',
|
||||
'قد يتم عرض معلومات تتعلق ببرنامج الولاء الخاص بـ RentalDriveGo للزوار، كما قد تُتاح لهم روابط للتنزيل عبر الموقع.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '3. الاتصال',
|
||||
paragraphs: [
|
||||
'يمكنك التواصل معنا عبر الموقع أو عبر البريد الإلكتروني على العنوان service-client@rentaldrivego.ma.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '4. قبول الشروط العامة للاستخدام',
|
||||
paragraphs: [
|
||||
'باستخدامك للموقع، فإنك توافق على هذه الشروط العامة للاستخدام وتتعهد بالالتزام بها. وإذا كنت لا تقبلها، فيجب عليك عدم استخدام الموقع.',
|
||||
'كما تنطبق سياسة الخصوصية وسياسة ملفات تعريف الارتباط الخاصة بنا على استخدامك للموقع.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '5. تعديل الشروط العامة للاستخدام',
|
||||
paragraphs: [
|
||||
'يجوز لنا تعديل هذه الشروط العامة للاستخدام في أي وقت. ولذلك ننصحك بالاطلاع على النسخة السارية كلما رغبت في استخدام الموقع.',
|
||||
'كما نحتفظ بالحق في تعديل أو تصحيح أو تحديث أي معلومات منشورة على الموقع في أي وقت.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '6. تعديل محتوى الموقع وتوفّره',
|
||||
paragraphs: [
|
||||
'يجوز لنا تحديث أو تعديل محتوى الموقع في أي وقت بما يعكس تطور احتياجات المستخدمين والشركاء.',
|
||||
'يتم توفير الموقع مجاناً. وعلى الرغم من أننا نبذل وسائل معقولة لمنع الانقطاعات وحلها، فإننا لا نضمن التوفر المستمر أو غير المنقطع للموقع.',
|
||||
'يجوز لنا تعليق أو سحب أو تقييد إتاحة كل الموقع أو جزء منه لأسباب تجارية أو تشغيلية.',
|
||||
'أنت مسؤول عن التأكد من أن جميع الأشخاص الذين يدخلون إلى الموقع عبر اتصالك بالإنترنت على علم بهذه الشروط ويلتزمون بها.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '7. نطاق المعلومات الواردة في الموقع',
|
||||
paragraphs: [
|
||||
'يتم تقديم محتوى الموقع لأغراض إعلامية عامة فقط، ولا يشكل خدمة توصية أو مشورة يمكن الاعتماد عليها.',
|
||||
'ورغم أننا نبذل جهوداً معقولة لتحديث المعلومات المنشورة على الموقع، فإننا لا نضمن، صراحةً أو ضمناً، أن يكون المحتوى دقيقاً أو كاملاً أو محدثاً.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '8. المسؤولية تجاه المواقع التابعة للغير',
|
||||
paragraphs: [
|
||||
'لا نتحمل المسؤولية عن المواقع الإلكترونية التابعة للغير التي قد يتضمن الموقع روابط إليها.',
|
||||
'أي روابط نحو محتوى أو مواقع أو موارد تابعة للغير يتم توفيرها لأغراض إعلامية فقط، ولا يمكن تحميلنا المسؤولية عن محتواها أو دقتها أو جودتها.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '9. تحديد المسؤولية',
|
||||
paragraphs: [
|
||||
'أنت توافق على أن الدخول إلى الموقع واستخدامه، وكذلك الصفحات المرتبطة به، يكون على مسؤوليتك الخاصة.',
|
||||
'لا تستبعد هذه الشروط ولا تحد من مسؤوليتنا في الحالات التي يكون فيها هذا الاستبعاد أو التحديد غير قانوني.',
|
||||
'ولا نحن ولا شركاؤنا أو الجهات المرتبطة بنا نتحمل أي مسؤولية عن الأضرار الناتجة عن استخدام المعلومات أو الملفات أو الروابط أو المواقع التي يمكن الوصول إليها من خلال الموقع أو استلامها عبره.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '10. مسؤولياتك',
|
||||
paragraphs: [
|
||||
'أنت تقر بأننا قد نستخدم المعلومات التي تقدمها لنا عندما تكون قد منحت الموافقة المطلوبة أو عندما يوجد أساس قانوني آخر لذلك.',
|
||||
'وتتعهد باستخدام الموقع فقط لأغراض مشروعة.',
|
||||
'وإذا رأينا أن هناك خرقاً لهذه الشروط العامة للاستخدام، فيجوز لنا اتخاذ أي إجراء نراه مناسباً.',
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: '11. الملكية الفكرية',
|
||||
paragraphs: [
|
||||
'الموقع ملك لنا، والموقع بكامله محمي بموجب القوانين المتعلقة بالملكية الفكرية.',
|
||||
'وجميع حقوق النسخ والتمثيل محفوظة.',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# Cookie Policy
|
||||
|
||||
**Platform:** RentalDriveGo
|
||||
**Domain:** rentaldrivego.ma
|
||||
**Last updated:** 2026-05-23
|
||||
|
||||
---
|
||||
|
||||
## 1. What Are Cookies
|
||||
|
||||
Cookies are small text files that a website stores on your device when you visit. They allow the site to remember information about your visit — such as your preferred language or whether you are signed in — so you do not have to re-enter it every time.
|
||||
|
||||
RentalDriveGo uses cookies only for the purposes described in this document. We do not use cookies to track your activity across third-party websites, serve advertisements, or build behavioural profiles.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types of Cookies We Use
|
||||
|
||||
We use two categories of cookies: **strictly necessary** and **preference/functional**.
|
||||
|
||||
We do not use analytics cookies, advertising cookies, or third-party tracking cookies.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cookie Details
|
||||
|
||||
### 3.1 Authentication Cookie
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | `employee_token` |
|
||||
| **Category** | Strictly necessary |
|
||||
| **Duration** | 8 hours (session) |
|
||||
| **Scope** | All pages (`path=/`) |
|
||||
| **Third-party** | No |
|
||||
|
||||
**Purpose:** This cookie is set when an employee or administrator signs in to the RentalDriveGo workspace. It stores a cryptographically signed token (JWT) that proves your identity to the platform. Without this cookie, the dashboard and protected API endpoints cannot verify who you are and will redirect you to the sign-in page.
|
||||
|
||||
**When it is set:** On a successful sign-in.
|
||||
**When it is removed:** When you sign out, or automatically after 8 hours of inactivity.
|
||||
**Can you opt out?** No. This cookie is required for the platform to function. Blocking it will prevent you from signing in.
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Language Preference Cookie
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | `rentaldrivego-language` |
|
||||
| **Category** | Functional / preference |
|
||||
| **Duration** | 1 year |
|
||||
| **Scope** | All pages (`path=/`) |
|
||||
| **Third-party** | No |
|
||||
|
||||
**Purpose:** Stores your chosen display language so the interface appears in your preferred language on every visit across all parts of the platform (public marketplace, dashboard, and admin panel). Supported values are English (`en`), French (`fr`), and Arabic (`ar`).
|
||||
|
||||
**When it is set:** When you change the language using the language selector, or automatically on first visit based on your browser's language settings.
|
||||
**Can you opt out?** You can block this cookie. If you do, the platform will fall back to a default language and you may need to select your language on every visit.
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Theme Preference Cookie
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | `rentaldrivego-theme` |
|
||||
| **Category** | Functional / preference |
|
||||
| **Duration** | 1 year |
|
||||
| **Scope** | All pages (`path=/`) |
|
||||
| **Third-party** | No |
|
||||
|
||||
**Purpose:** Stores your preferred colour scheme — light mode or dark mode. This cookie is read immediately when a page loads (before the interface is drawn) to apply the correct theme without any visible flash of the wrong colours.
|
||||
|
||||
**When it is set:** When you toggle the light/dark mode switch.
|
||||
**Can you opt out?** You can block this cookie. If you do, the platform will fall back to your operating system's colour scheme preference and the theme may reset on every visit.
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Per-User Scoped Preference Cookies
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Name** | `rentaldrivego-language--{userID}`, `rentaldrivego-theme--{userID}` |
|
||||
| **Category** | Functional / preference |
|
||||
| **Duration** | 1 year |
|
||||
| **Scope** | All pages (`path=/`) |
|
||||
| **Third-party** | No |
|
||||
|
||||
**Purpose:** When you are signed in, the platform saves your language and theme preferences under a cookie that is tied to your account identifier. This allows multiple employees who share a device (for example, in a shared office environment) to each maintain independent preferences. The `{userID}` portion is derived from the authentication token and does not contain personal information beyond an internal account identifier.
|
||||
|
||||
**When it is set:** On every language or theme change while you are signed in.
|
||||
**Can you opt out?** You can block these cookies. If you do, your preferences will revert to the shared or default settings each time you sign in on a shared device.
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Legacy Preference Cookies (Transitional)
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Names** | `dashboard-language`, `marketplace-language` |
|
||||
| **Category** | Functional / preference |
|
||||
| **Duration** | 1 year |
|
||||
| **Scope** | All pages (`path=/`) |
|
||||
| **Third-party** | No |
|
||||
|
||||
**Purpose:** These cookies exist for backward compatibility with older versions of the platform. They store the same language preference as `rentaldrivego-language` but were scoped to individual sub-applications. New code always reads `rentaldrivego-language` first and only falls back to these if the primary cookie is absent.
|
||||
|
||||
**Planned removal:** These cookies will be removed once all users have transitioned to the unified preference system. They do not store any additional personal data.
|
||||
|
||||
---
|
||||
|
||||
## 4. What We Do Not Do
|
||||
|
||||
- We do **not** use cookies to track you across other websites.
|
||||
- We do **not** share cookie data with advertisers or data brokers.
|
||||
- We do **not** use session-replay or behavioural analytics cookies.
|
||||
- We do **not** set cookies from third-party domains on our pages.
|
||||
|
||||
---
|
||||
|
||||
## 5. Cookie Consent
|
||||
|
||||
**Strictly necessary cookies** (section 3.1) do not require your consent because they are essential to provide the service you have requested.
|
||||
|
||||
**Functional / preference cookies** (sections 3.2–3.5) are used solely to remember your choices and improve your experience. They do not process personal data for marketing or tracking purposes. Under most privacy regulations (including GDPR), these may be set without explicit consent when used exclusively to fulfil user-requested functionality. Where local law requires explicit consent, a consent prompt will be displayed on your first visit.
|
||||
|
||||
---
|
||||
|
||||
## 6. How to Manage or Delete Cookies
|
||||
|
||||
You can control cookies through your browser settings. Common options include:
|
||||
|
||||
- **Block all cookies** — the platform will work but you will need to re-enter preferences on every visit and you will not be able to sign in.
|
||||
- **Block third-party cookies** — this will have no effect on RentalDriveGo as we do not use third-party cookies.
|
||||
- **Delete cookies on close** — you will be signed out and your preferences will reset each time you close the browser.
|
||||
- **Delete specific cookies** — use your browser's developer tools (Application → Cookies) to remove individual cookies by name.
|
||||
|
||||
Browser-specific instructions:
|
||||
|
||||
| Browser | Settings location |
|
||||
|---|---|
|
||||
| Chrome | Settings → Privacy and security → Cookies and other site data |
|
||||
| Firefox | Settings → Privacy & Security → Cookies and Site Data |
|
||||
| Safari | Settings → Privacy → Manage Website Data |
|
||||
| Edge | Settings → Cookies and site permissions → Cookies and site data |
|
||||
|
||||
---
|
||||
|
||||
## 7. Security
|
||||
|
||||
The authentication cookie (`employee_token`) is signed and expires after 8 hours. It is transmitted over HTTPS in production. We recommend using the platform on trusted devices only and signing out when you are finished.
|
||||
|
||||
Note for our technical team: the `HttpOnly` and `Secure` flags should be added to `employee_token` in a future release to further limit exposure to XSS and mixed-content attacks.
|
||||
|
||||
---
|
||||
|
||||
## 8. Changes to This Policy
|
||||
|
||||
We may update this Cookie Policy when we add new features or change how existing cookies work. The "Last updated" date at the top of this document will reflect any changes. Significant changes will be communicated through the platform or by email.
|
||||
|
||||
---
|
||||
|
||||
## 9. Contact
|
||||
|
||||
If you have questions about this Cookie Policy or how we handle your data, please contact us at:
|
||||
|
||||
**Email:** rentaldrivego@gmail.com
|
||||
**Website:** https://rentaldrivego.ma
|
||||
@@ -11,6 +11,7 @@ PROJECT_NAME="${DOCKER_PROD_PROJECT_NAME:-rentaldrivego-prod}"
|
||||
PORTAINER_PROJECT_NAME="${DOCKER_PORTAINER_PROJECT_NAME:-rentaldrivego-portainer-prod}"
|
||||
TRAEFIK_NETWORK="${DOCKER_PROD_TRAEFIK_NETWORK:-traefik-proxy}"
|
||||
VOLUME_BACKUP_IMAGE="${DOCKER_VOLUME_BACKUP_IMAGE:-postgres:16-alpine}"
|
||||
API_UPLOADS_VOLUME="${DOCKER_PROD_API_UPLOADS_VOLUME:-${PROJECT_NAME}_api_uploads}"
|
||||
|
||||
ensure_env_file() {
|
||||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||
@@ -27,6 +28,13 @@ ensure_traefik_network() {
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_api_uploads_volume() {
|
||||
if ! docker volume inspect "${API_UPLOADS_VOLUME}" >/dev/null 2>&1; then
|
||||
echo "Creating Docker volume: ${API_UPLOADS_VOLUME}"
|
||||
docker volume create "${API_UPLOADS_VOLUME}" >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
prod_compose() {
|
||||
docker compose -p "${PROJECT_NAME}" --env-file "${ENV_FILE}" -f "${PROD_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
@@ -41,6 +49,7 @@ traefik_compose() {
|
||||
|
||||
start_prod_services() {
|
||||
ensure_env_file
|
||||
ensure_api_uploads_volume
|
||||
prod_compose up --build -d "$@"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user