4 Commits

Author SHA1 Message Date
ISMAIL MASSERAN db3ac37042 DONE: update prefix for BOARD_MEETING_REFERENCE_PREFIX, add placeholder for address
Build Docker Image / build-backend (push) Successful in 2m52s
Build Docker Image / build-frontend (push) Successful in 19s
2026-08-09 11:46:37 +08:00
ISMAIL MASSERAN 605ccf5a7b DONE: increase limit size of profile pic, fix swal error when uploading profile pic large than 2 mb on prod
Build Docker Image / build-backend (push) Successful in 48s
Build Docker Image / build-frontend (push) Successful in 10s
2026-07-19 12:58:47 +08:00
ismailmasseran 06b687f408 DONE: fix bypass password on production not working (#14)
Build Docker Image / build-backend (push) Successful in 4m51s
Build Docker Image / build-frontend (push) Successful in 15s
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #14
2026-07-16 11:56:03 +08:00
ismailmasseran d0f5e368e3 DONE: fix password bypass in production, use dropdown for admin to edit membership application (#13)
Build Docker Image / build-backend (push) Successful in 5m31s
Build Docker Image / build-frontend (push) Successful in 28s
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Reviewed-on: #13
2026-07-16 11:02:14 +08:00
16 changed files with 131 additions and 129 deletions
+43 -18
View File
@@ -1,9 +1,12 @@
APP_NAME="SUTERA 3.0 Dev"
APP_NAME="MYKOPKB 1.0"
APP_ENV=local
APP_KEY=base64:0dM5HYEFRu9P6ni51QTUZm5mQ23znc/s/aebLTRUGxU=
APP_DEBUG=true
APP_TIMEZONE=Asia/Kuala_Lumpur
APP_URL=http://localhost
FRONTEND_URL=http://localhost:5173
TELESCOPE_ENABLED=true
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
@@ -24,8 +27,8 @@ LOG_LEVEL=debug
DB_CONNECTION=pgsql
DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=sutera
DB_USERNAME=suterauser
DB_DATABASE=mykopkb
DB_USERNAME=mykopkbuser
DB_PASSWORD=password
SESSION_DRIVER=redis
@@ -38,8 +41,11 @@ BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
HORIZON_WORKER_MEMORY=512
HORIZON_WORKER_TIMEOUT=3600
CACHE_STORE=redis
CACHE_PREFIX=sutera_cache
CACHE_PREFIX=mykopkb_cache
MEMCACHED_HOST=memcached
@@ -54,8 +60,9 @@ MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_ADDRESS="no-reply@army.mil.my"
MAIL_FROM_NAME="${APP_NAME}"
MAIL_LOGO_PATH="images/logo/logo-kopkb-letterhead.svg"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
@@ -73,18 +80,6 @@ VITE_APP_NAME="${APP_NAME}"
EXTERNAL_API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWJzcC1yZXNvdXJjZUlkIl0sInRpbWVvdXRQZXJpb2QiOjkwMCwidXNlcl9uYW1lIjoic3BhLXVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNzI4NTgzNjEyLCJqdGkiOiJjMmU2OTkxOS0yZDU3LTQ5YmYtYTVmZS1lZjVmMzIyNDZkZDIiLCJjbGllbnRfaWQiOiJzcGEtY2xpZW50In0.LE2byaRzsYkchzI4ox9Gz2u6jNmTqZQa-_ttR2PY8e8"
EXTERNAL_API_BASE_URL=http://20.11.32.134/rest/td/gasset/asset
SSO_SECRET="Y6g#Rb@Km=Nz]o^X+fEC~<W/LP%qh;M}9T&v!["
SSO_VALIDATION_URL=http://host.docker.internal:8080/api/sso-validate
SSO_MAX_ATTEMPTS=5
SSO_TOKEN_EXPIRATION=300
SSO_AUTO_ACTIVATE_USERS=true
SSO_LOG_ACTIVITIES=true
SSO_CONNECTION_TIMEOUT=10
SSO_RESPONSE_TIMEOUT=30
SSO_PROXY_URL=http://20.9.91.1:3128
SSO_ENABLE_FALLBACK=true
SSO_PROXY_ENABLED=false
SANCTUM_STATEFUL_DOMAINS=http://localhost
BLOCK_API_TOOLS_IN_PRODUCTION=true
@@ -97,5 +92,35 @@ ENABLE_API_KEY_AUTH=true
API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae
API_LOG_KEY_USAGE=true
# HttpOnly Sanctum token cookie (set on api.* domain)
AUTH_COOKIE_NAME=auth_token
AUTH_COOKIE_LIFETIME=360
AUTH_COOKIE_SECURE=true
AUTH_COOKIE_SAME_SITE=lax
AUTH_COOKIE_EXPOSE_TOKEN=false
# Browsershot (PDF generation via headless Chrome + Puppeteer in be/)
# Required in Docker/Sail. After first sail up: sail npm install && sail npx puppeteer browsers install chrome-headless-shell
BROWSERSHOT_NO_SANDBOX=true
# BROWSERSHOT_NODE_BINARY=
# BROWSERSHOT_NPM_BINARY=
# BROWSERSHOT_NODE_MODULE_PATH=
# BROWSERSHOT_CHROME_PATH=
# BROWSERSHOT_TIMEOUT=60
PUBLIC_PROFILE_TOKEN_TTL_DAYS=7
FRONTEND_URL=https://mykopkb.koppkb.com
EMAIL_VERIFICATION_EXPIRE_MINUTES=60
EMAIL_VERIFICATION_REDIRECT_PATH=/profile-overview-2
PHONE_VERIFICATION_OTP_EXPIRY_MINUTES=10
PHONE_VERIFICATION_TOKEN_EXPIRY_MINUTES=30
PHONE_VERIFICATION_OTP_MAX_ATTEMPTS=5
SMS_DRIVER=log
ONEWAYSMS_BASE_URL=http://gateway.onewaysms.com.my:10001/api.aspx
ONEWAYSMS_API_USERNAME=API7LO0ELJTAU
ONEWAYSMS_API_PASSWORD=API7LO0ELJTAU7LO0E
ONEWAYSMS_SENDER_ID=INFO
EXTERNAL_SYSTEM_SSO_ISSUER=mykopkb
-88
View File
@@ -1,88 +0,0 @@
APP_NAME="SUTERA 3.0"
APP_ENV=staging
APP_KEY=base64:9vnA0vvAweC8C+/WKyEDOziWp4QPsdKZ/blgDZXIbKQ=
APP_DEBUG=false
APP_TIMEZONE=Asia/Kuala_Lumpur
APP_URL=https://sutera.koppkb.com
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=8
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=error
DB_CONNECTION=pgsql
DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=sutera
DB_USERNAME=suterauser
DB_PASSWORD=Aretus@2025
DB_ROOT_PASSWORD=Sutera_Staging@root_2025
SESSION_DRIVER=redis
SESSION_LIFETIME=60
SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=sutera_cache
CACHE_DRIVER=redis
MEMCACHED_HOST=memcached
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=sutera_redis@2025
REDIS_DB=0
MAIL_MAILER=smtp
MAIL_SCHEME=null
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
# VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
# VITE_PUSHER_HOST="${PUSHER_HOST}"
# VITE_PUSHER_PORT="${PUSHER_PORT}"
# VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
# VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
EXTERNAL_API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWJzcC1yZXNvdXJjZUlkIl0sInRpbWVvdXRQZXJpb2QiOjkwMCwidXNlcl9uYW1lIjoic3BhLXVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNzI4NTgzNjEyLCJqdGkiOiJjMmU2OTkxOS0yZDU3LTQ5YmYtYTVmZS1lZjVmMzIyNDZkZDIiLCJjbGllbnRfaWQiOiJzcGEtY2xpZW50In0.LE2byaRzsYkchzI4ox9Gz2u6jNmTqZQa-_ttR2PY8e8"
EXTERNAL_API_BASE_URL=http://20.11.32.134/rest/td/gasset/asset
SSO_SECRET="Y6g#Rb@Km=Nz]o^X+fEC~<W/LP%qh;M}9T&v!["
SSO_VALIDATION_URL=http://host.docker.internal:8080/api/sso-validate
SSO_MAX_ATTEMPTS=5
SSO_TOKEN_EXPIRATION=300
SSO_AUTO_ACTIVATE_USERS=true
SSO_LOG_ACTIVITIES=true
SSO_CONNECTION_TIMEOUT=10
SSO_RESPONSE_TIMEOUT=30
SSO_PROXY_URL=http://20.9.91.1:3128
SSO_ENABLE_FALLBACK=true
@@ -8,8 +8,7 @@
@section('letter-recipient')
<strong>{{ $applicant->name }}</strong><br>
{!! nl2br(e($applicant->address)) !!}<br>
{{ $applicant->postcode }}
{!! nl2br(e($applicant->address)) !!}
@endsection
@section('letter-honorific')
@@ -122,7 +122,7 @@ class UserController extends BaseCrudController
'ic_number' => 'sometimes|string|max:255|unique:users,ic_number,'.$user->id,
'position' => 'sometimes|string|max:255',
'phone_number' => 'sometimes|string|max:255',
'image' => 'sometimes|image|mimes:jpeg,png,jpg,gif|max:2048',
'image' => 'sometimes|image|mimes:jpeg,png,jpg,gif|max:5120',
'image_url' => 'sometimes|string|max:255',
'gender' => 'sometimes|string|max:255',
'marriage_status' => 'sometimes|string|max:255',
+23 -11
View File
@@ -94,19 +94,31 @@ class FortifyServiceProvider extends ServiceProvider
Fortify::authenticateUsing(function (Request $request) {
$user = User::where('email', $request->email)->first();
// 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()],
]);
}
$invalidCredentialsMessage = 'Emel atau kata laluan tidak sah.';
return $user;
$failLogin = function () use ($invalidCredentialsMessage): never {
throw ValidationException::withMessages([
'email' => [$invalidCredentialsMessage],
]);
};
if (! $user) {
$failLogin();
}
$shouldBypassPassword = app()->environment('local');
if (! $shouldBypassPassword && ! Hash::check($request->password, $user->password)) {
$failLogin();
}
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)
$exceptions->render(function (Illuminate\Validation\ValidationException $e, $request) {
if ($request->expectsJson()) {
$errors = $e->errors();
$firstMessage = collect($errors)->flatten()->first() ?? 'Data tidak sah.';
return response()->json([
'success' => false,
'message' => 'Data tidak sah.',
'errors' => $e->errors(),
'message' => $firstMessage,
'errors' => $errors,
], 422);
}
});
@@ -9,6 +9,8 @@ http {
sendfile on;
keepalive_timeout 65;
client_max_body_size 128M;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
+2
View File
@@ -8,6 +8,8 @@ http {
sendfile on;
keepalive_timeout 65;
client_max_body_size 128M;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
+2
View File
@@ -9,6 +9,8 @@ http {
sendfile on;
keepalive_timeout 65;
client_max_body_size 128M;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
+1 -1
View File
@@ -23,7 +23,7 @@ export const EMPLOYERS = [
},
{
name: 'Xtra Biz Sdn Bhd (XBSB)',
address: "Lot 241, Jalan Dato' Cik Arif, Taman Mutiara 1, 15000 Kota Bharu, Kelantan.",
address: "-",
},
{
name: 'Pesara',
@@ -692,7 +692,12 @@ function stepLabelClass(stepId: number) {
<template v-else-if="currentStep === 2">
<Field class="col-span-12">
<FieldLabel for="address">Alamat</FieldLabel>
<Textarea id="address" v-model="form.applicant.address" rows="3" />
<Textarea
id="address"
v-model="form.applicant.address"
rows="3"
placeholder="Lot Pt 448, Tingkat 1, Jalan Kuala Krai, Batu 3, Wakaf Che Yeh, 15150 Kota Bharu, Kelantan."
/>
<FieldError v-if="fieldErrors['applicant.address']">{{ fieldErrors['applicant.address'] }}</FieldError>
</Field>
<Field class="col-span-12 sm:col-span-4">
@@ -65,7 +65,7 @@ const DOCUMENT_TYPE_LABELS: Record<string, string> = {
[RESULT_LETTER_DOCUMENT_TYPE]: 'Surat Keputusan',
}
const BOARD_MEETING_REFERENCE_PREFIX = 'Mesyuarat Lembaga Bil.'
const BOARD_MEETING_REFERENCE_PREFIX = 'Mesyuarat Lembaga KoPKB Bil. '
const router = useRouter()
const route = useRoute()
@@ -58,6 +58,7 @@ import { ADMIN_ATTACHMENT_DOCUMENT_TYPE } from '../types/membership-application.
import {
APPLICANT_DOCUMENT_UPLOAD_TYPES,
DOCUMENT_TYPE_LABELS,
EMPLOYER_OPTIONS,
GENDER_OPTIONS,
MARRIAGE_STATUS_OPTIONS,
RELATIONSHIP_OPTIONS,
@@ -67,6 +68,7 @@ import {
createEmptyFormState,
createSelectCollection,
detailToFormState,
getEmployerAddress,
labelToApiValue,
} from '../utils/membership-application-form.utils'
@@ -112,6 +114,7 @@ const referenceLookupLoading = reactive({
const genderCollection = createSelectCollection(GENDER_OPTIONS)
const marriageStatusCollection = createSelectCollection(MARRIAGE_STATUS_OPTIONS)
const relationshipCollection = createSelectCollection(RELATIONSHIP_OPTIONS)
const employerCollection = createSelectCollection(EMPLOYER_OPTIONS)
const canEdit = computed(
() =>
@@ -123,6 +126,7 @@ const genderInitial = computed(() => apiValueToLabel(GENDER_OPTIONS, form.applic
const marriageStatusInitial = computed(() =>
apiValueToLabel(MARRIAGE_STATUS_OPTIONS, form.applicant.marriage_status),
)
const employerInitial = computed(() => apiValueToLabel(EMPLOYER_OPTIONS, form.applicant.employer_name))
const workflowProgress = computed(() =>
application.value ? getWorkflowProgress(application.value.status) : null,
@@ -309,6 +313,14 @@ function setMarriageStatusValue(details: { value: string[] }) {
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[] }) {
const heir = form.heirs[index]
if (!heir) return
@@ -859,8 +871,24 @@ onUnmounted(() => {
<TabsContent value="employment" class="mt-6">
<div class="grid grid-cols-12 gap-4 gap-y-5">
<Field class="col-span-12 sm:col-span-6">
<FieldLabel for="employer_name">Nama Majikan</FieldLabel>
<Input id="employer_name" v-model="form.applicant.employer_name" type="text" :disabled="!canEdit" />
<FieldLabel>Nama Majikan</FieldLabel>
<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>
</Field>
@@ -872,7 +900,7 @@ onUnmounted(() => {
</Field>
<Field class="col-span-12">
<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>
</Field>
@@ -1,5 +1,6 @@
import dayjs from 'dayjs'
import * as select from '@zag-js/select'
import { EMPLOYERS } from '@/constants/employers'
import type {
MembershipApplicationDetail,
MembershipApplicationDocumentsForm,
@@ -35,6 +36,15 @@ export const RELATIONSHIP_OPTIONS: SelectOption[] = [
{ 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> = {
ic_copy: 'Salinan Kad Pengenalan',
photo: 'Gambar Passport',
@@ -190,6 +190,9 @@ onMounted(async () => {
No. {{ authStore.userMemberNumber }}
</Badge>
</div>
<p class="mt-2 text-xs opacity-70">
Gambar profil: maksimum 2 MB (JPEG, PNG, GIF).
</p>
</div>
</div>
-1
View File
@@ -318,7 +318,6 @@ async function confirmRestore() {
const headers: TableHeader[] = [
{ title: 'Bil.', key: '#', sortable: false },
{ title: 'Name', key: 'name', sortable: true },
{ title: 'Jenis Anggota', key: 'member_type', sortable: true },
{
title: 'Unit',
key: 'company_name',