DONE: fix password bypass in production, use dropdown for admin to edit membership application #13
@@ -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.';
|
||||||
// Bypass password check in local or development environments
|
|
||||||
$shouldBypassPassword = config('app.env', 'local');
|
|
||||||
|
|
||||||
if ($user && ($shouldBypassPassword || Hash::check($request->password, $user->password))) {
|
|
||||||
if (! $user->canAuthenticate()) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'email' => [$user->getLoginRestrictionMessage()],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $user;
|
$failLogin = function () use ($invalidCredentialsMessage): never {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => [$invalidCredentialsMessage],
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
$failLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$shouldBypassPassword = config('app.env', 'local');
|
||||||
|
|
||||||
|
if (! $shouldBypassPassword && ! Hash::check($request->password, $user->password)) {
|
||||||
|
$failLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user->canAuthenticate()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => [$user->getLoginRestrictionMessage()],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user