DONE: fix password bypass in production, use dropdown for admin to edit membership application #13

Merged
ismailmasseran merged 1 commits from testcase/fix-feature into main 2026-07-16 11:02:14 +08:00
5 changed files with 70 additions and 17 deletions
+21 -9
View File
@@ -94,19 +94,31 @@ class FortifyServiceProvider extends ServiceProvider
Fortify::authenticateUsing(function (Request $request) { Fortify::authenticateUsing(function (Request $request) {
$user = User::where('email', $request->email)->first(); $user = User::where('email', $request->email)->first();
$invalidCredentialsMessage = 'Emel atau kata laluan tidak sah.';
$failLogin = function () use ($invalidCredentialsMessage): never {
throw ValidationException::withMessages([
'email' => [$invalidCredentialsMessage],
]);
};
if (! $user) {
$failLogin();
}
// Bypass password check in local or development environments
$shouldBypassPassword = config('app.env', 'local'); $shouldBypassPassword = config('app.env', 'local');
if ($user && ($shouldBypassPassword || Hash::check($request->password, $user->password))) { if (! $shouldBypassPassword && ! Hash::check($request->password, $user->password)) {
if (! $user->canAuthenticate()) { $failLogin();
throw ValidationException::withMessages([
'email' => [$user->getLoginRestrictionMessage()],
]);
}
return $user;
} }
if (! $user->canAuthenticate()) {
throw ValidationException::withMessages([
'email' => [$user->getLoginRestrictionMessage()],
]);
}
return $user;
}); });
} }
} }
+5 -2
View File
@@ -59,10 +59,13 @@ return Application::configure(basePath: dirname(__DIR__))
// Handle Validation exceptions (422 errors) // Handle Validation exceptions (422 errors)
$exceptions->render(function (Illuminate\Validation\ValidationException $e, $request) { $exceptions->render(function (Illuminate\Validation\ValidationException $e, $request) {
if ($request->expectsJson()) { if ($request->expectsJson()) {
$errors = $e->errors();
$firstMessage = collect($errors)->flatten()->first() ?? 'Data tidak sah.';
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Data tidak sah.', 'message' => $firstMessage,
'errors' => $e->errors(), 'errors' => $errors,
], 422); ], 422);
} }
}); });
+1 -1
View File
@@ -23,7 +23,7 @@ export const EMPLOYERS = [
}, },
{ {
name: 'Xtra Biz Sdn Bhd (XBSB)', name: 'Xtra Biz Sdn Bhd (XBSB)',
address: "Lot 241, Jalan Dato' Cik Arif, Taman Mutiara 1, 15000 Kota Bharu, Kelantan.", address: "-",
}, },
{ {
name: 'Pesara', name: 'Pesara',
@@ -58,6 +58,7 @@ import { ADMIN_ATTACHMENT_DOCUMENT_TYPE } from '../types/membership-application.
import { import {
APPLICANT_DOCUMENT_UPLOAD_TYPES, APPLICANT_DOCUMENT_UPLOAD_TYPES,
DOCUMENT_TYPE_LABELS, DOCUMENT_TYPE_LABELS,
EMPLOYER_OPTIONS,
GENDER_OPTIONS, GENDER_OPTIONS,
MARRIAGE_STATUS_OPTIONS, MARRIAGE_STATUS_OPTIONS,
RELATIONSHIP_OPTIONS, RELATIONSHIP_OPTIONS,
@@ -67,6 +68,7 @@ import {
createEmptyFormState, createEmptyFormState,
createSelectCollection, createSelectCollection,
detailToFormState, detailToFormState,
getEmployerAddress,
labelToApiValue, labelToApiValue,
} from '../utils/membership-application-form.utils' } from '../utils/membership-application-form.utils'
@@ -112,6 +114,7 @@ const referenceLookupLoading = reactive({
const genderCollection = createSelectCollection(GENDER_OPTIONS) const genderCollection = createSelectCollection(GENDER_OPTIONS)
const marriageStatusCollection = createSelectCollection(MARRIAGE_STATUS_OPTIONS) const marriageStatusCollection = createSelectCollection(MARRIAGE_STATUS_OPTIONS)
const relationshipCollection = createSelectCollection(RELATIONSHIP_OPTIONS) const relationshipCollection = createSelectCollection(RELATIONSHIP_OPTIONS)
const employerCollection = createSelectCollection(EMPLOYER_OPTIONS)
const canEdit = computed( const canEdit = computed(
() => () =>
@@ -123,6 +126,7 @@ const genderInitial = computed(() => apiValueToLabel(GENDER_OPTIONS, form.applic
const marriageStatusInitial = computed(() => const marriageStatusInitial = computed(() =>
apiValueToLabel(MARRIAGE_STATUS_OPTIONS, form.applicant.marriage_status), apiValueToLabel(MARRIAGE_STATUS_OPTIONS, form.applicant.marriage_status),
) )
const employerInitial = computed(() => apiValueToLabel(EMPLOYER_OPTIONS, form.applicant.employer_name))
const workflowProgress = computed(() => const workflowProgress = computed(() =>
application.value ? getWorkflowProgress(application.value.status) : null, application.value ? getWorkflowProgress(application.value.status) : null,
@@ -309,6 +313,14 @@ function setMarriageStatusValue(details: { value: string[] }) {
delete fieldErrors['applicant.marriage_status'] delete fieldErrors['applicant.marriage_status']
} }
function setEmployerValue(details: { value: string[] }) {
const employerName = labelToApiValue(EMPLOYER_OPTIONS, details.value[0])
form.applicant.employer_name = employerName
form.applicant.employer_address = getEmployerAddress(employerName)
delete fieldErrors['applicant.employer_name']
delete fieldErrors['applicant.employer_address']
}
function setHeirRelationshipValue(index: number, details: { value: string[] }) { function setHeirRelationshipValue(index: number, details: { value: string[] }) {
const heir = form.heirs[index] const heir = form.heirs[index]
if (!heir) return if (!heir) return
@@ -859,8 +871,24 @@ onUnmounted(() => {
<TabsContent value="employment" class="mt-6"> <TabsContent value="employment" class="mt-6">
<div class="grid grid-cols-12 gap-4 gap-y-5"> <div class="grid grid-cols-12 gap-4 gap-y-5">
<Field class="col-span-12 sm:col-span-6"> <Field class="col-span-12 sm:col-span-6">
<FieldLabel for="employer_name">Nama Majikan</FieldLabel> <FieldLabel>Nama Majikan</FieldLabel>
<Input id="employer_name" v-model="form.applicant.employer_name" type="text" :disabled="!canEdit" /> <SelectRoot :key="`employer-${form.applicant.employer_name}`" class="w-full"
:collection="employerCollection" :default-value="employerInitial" :disabled="!canEdit"
@value-change="setEmployerValue">
<SelectControl>
<SelectTrigger :aria-invalid="!!fieldErrors['applicant.employer_name']">
<SelectValueText placeholder="Pilih majikan" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel>Nama Majikan</SelectItemGroupLabel>
<SelectItem v-for="item in employerCollection.items" :key="item.label" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
<FieldError v-if="fieldErrors['applicant.employer_name']">{{ fieldErrors['applicant.employer_name'] }} <FieldError v-if="fieldErrors['applicant.employer_name']">{{ fieldErrors['applicant.employer_name'] }}
</FieldError> </FieldError>
</Field> </Field>
@@ -872,7 +900,7 @@ onUnmounted(() => {
</Field> </Field>
<Field class="col-span-12"> <Field class="col-span-12">
<FieldLabel for="employer_address">Alamat Majikan</FieldLabel> <FieldLabel for="employer_address">Alamat Majikan</FieldLabel>
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" :disabled="!canEdit" /> <Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" disabled />
<FieldError v-if="fieldErrors['applicant.employer_address']">{{ fieldErrors['applicant.employer_address'] <FieldError v-if="fieldErrors['applicant.employer_address']">{{ fieldErrors['applicant.employer_address']
}}</FieldError> }}</FieldError>
</Field> </Field>
@@ -1,5 +1,6 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import * as select from '@zag-js/select' import * as select from '@zag-js/select'
import { EMPLOYERS } from '@/constants/employers'
import type { import type {
MembershipApplicationDetail, MembershipApplicationDetail,
MembershipApplicationDocumentsForm, MembershipApplicationDocumentsForm,
@@ -35,6 +36,15 @@ export const RELATIONSHIP_OPTIONS: SelectOption[] = [
{ label: 'Lain-lain', value: 'Lain-lain' }, { label: 'Lain-lain', value: 'Lain-lain' },
] ]
export const EMPLOYER_OPTIONS: SelectOption[] = EMPLOYERS.map((employer) => ({
label: employer.name,
value: employer.name,
}))
export function getEmployerAddress(name: string): string {
return EMPLOYERS.find((employer) => employer.name === name)?.address ?? ''
}
export const DOCUMENT_TYPE_LABELS: Record<string, string> = { export const DOCUMENT_TYPE_LABELS: Record<string, string> = {
ic_copy: 'Salinan Kad Pengenalan', ic_copy: 'Salinan Kad Pengenalan',
photo: 'Gambar Passport', photo: 'Gambar Passport',