diff --git a/TODO.md b/TODO.md index 6014041..8b98b0f 100644 --- a/TODO.md +++ b/TODO.md @@ -5,15 +5,15 @@ ## Improvement [ ] mesyuarat anggota tertinggi [ ] penyata anggota -[ ] daftar anggota +[x] daftar anggota [ ] baki pinjaman - transaction history: tarik dari ubs [ ] pembiayaan anggota [ ] sumbangan -[ ] wasi/penama +[x] wasi/penama [ ] pendaftaran anggota/meneruskan anggota/pencen [ ] daftar lembaga (backdated) -[ ] boleh print semua borang -[ ] jana surat lepas lulus anggota +[x] boleh print semua borang +[x] jana surat lepas lulus anggota ## Present to Boss (2/7/2026) [x] discuss logo baru MyKOPKB diff --git a/be/.env.example b/be/.env.example index cc40028..6b7e521 100644 --- a/be/.env.example +++ b/be/.env.example @@ -3,6 +3,7 @@ APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://localhost +FRONTEND_URL=http://localhost:5173 APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -87,3 +88,6 @@ AUTH_COOKIE_EXPOSE_TOKEN=false ACTIVE_ROLE_PREFER_MEMBER=true ACTIVE_ROLE_MEMBER_REDIRECT=/profile ACTIVE_ROLE_ADMIN_REDIRECT=/profile + +EMAIL_VERIFICATION_EXPIRE_MINUTES=60 +EMAIL_VERIFICATION_REDIRECT_PATH=/profile-overview-2 diff --git a/be/Modules/Auth/Actions/Fortify/CreateNewUser.php b/be/Modules/Auth/Actions/Fortify/CreateNewUser.php index 932ce8e..543bf93 100644 --- a/be/Modules/Auth/Actions/Fortify/CreateNewUser.php +++ b/be/Modules/Auth/Actions/Fortify/CreateNewUser.php @@ -12,7 +12,6 @@ use Laravel\Fortify\Contracts\CreatesNewUsers; use Modules\Auth\Entities\User; use Modules\Role\Entities\Role; use Modules\User\Notifications\UserActivationNotification; -use Modules\Auth\Services\EmailVerificationOtpService; use Exception; class CreateNewUser implements CreatesNewUsers @@ -54,38 +53,6 @@ class CreateNewUser implements CreatesNewUsers $user->assignRole($role); } - app(EmailVerificationOtpService::class)->send($user); - - // Send notification to admins if user requires activation - if ($user->status === 'pending') { - $this->notifyAdminsForActivation($user); - } - return $user; } - - /** - * Notify admins about new user requiring activation - */ - private function notifyAdminsForActivation(User $newUser): void - { - try { - $adminRoles = ['PENTADBIR', 'PS 2 KJC', 'PS 2 ALAT']; - - // Get users with specific roles plus admins (PENTADBIR and DEVELOPER) - $adminUsers = $this->getUsersWithRolesAndAdmins($adminRoles); - - $sender = auth()->user() ?? $newUser; // Use current user as sender, or new user if no auth - - foreach ($adminUsers as $admin) { - try { - $admin->notify(new UserActivationNotification($newUser, $sender)); - } catch (Exception $e) { - Log::error('Failed to send user activation notification: '.$e->getMessage()); - } - } - } catch (Exception $e) { - Log::error('Failed to notify admins for user activation: '.$e->getMessage()); - } - } } diff --git a/be/Modules/Auth/Actions/Fortify/LoginResponse.php b/be/Modules/Auth/Actions/Fortify/LoginResponse.php index 5226f5a..4851320 100644 --- a/be/Modules/Auth/Actions/Fortify/LoginResponse.php +++ b/be/Modules/Auth/Actions/Fortify/LoginResponse.php @@ -3,39 +3,19 @@ namespace Modules\Auth\Actions\Fortify; use Illuminate\Http\JsonResponse; -use Illuminate\Support\Facades\Auth; use Laravel\Fortify\Contracts\LoginResponse as ContractsLoginResponse; use Modules\Auth\Services\AuthSessionService; -use Modules\Auth\Services\EmailVerificationOtpService; class LoginResponse implements ContractsLoginResponse { public function __construct( protected AuthSessionService $authSession, - protected EmailVerificationOtpService $otpService, ) {} public function toResponse($request): JsonResponse { - $user = $request->user(); - - if (! $user->hasVerifiedEmail()) { - $this->otpService->send($user); - - Auth::guard(config('fortify.guard'))->logout(); - - return response()->json([ - 'success' => true, - 'message' => 'Sila semak e-mel anda untuk kod pengesahan 6 digit.', - 'data' => [ - 'email' => $user->email, - 'requires_email_verification' => true, - ], - ]); - } - return $this->authSession->createAuthResponse( - $user, + $request->user(), 'Login successful' ); } diff --git a/be/Modules/Auth/Actions/Fortify/RegisterResponse.php b/be/Modules/Auth/Actions/Fortify/RegisterResponse.php index c3d2487..be1850b 100644 --- a/be/Modules/Auth/Actions/Fortify/RegisterResponse.php +++ b/be/Modules/Auth/Actions/Fortify/RegisterResponse.php @@ -3,24 +3,21 @@ namespace Modules\Auth\Actions\Fortify; use Illuminate\Http\JsonResponse; -use Illuminate\Support\Facades\Auth; use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract; +use Modules\Auth\Services\AuthSessionService; class RegisterResponse implements RegisterResponseContract { + public function __construct( + protected AuthSessionService $authSession, + ) {} + public function toResponse($request): JsonResponse { - $email = $request->user()?->email; - - Auth::guard(config('fortify.guard'))->logout(); - - return response()->json([ - 'success' => true, - 'message' => 'Pendaftaran berjaya. Sila semak e-mel anda untuk kod pengesahan 6 digit.', - 'data' => [ - 'email' => $email, - 'requires_email_verification' => true, - ], - ], 201); + return $this->authSession->createAuthResponse( + $request->user(), + 'Pendaftaran berjaya. Akaun anda sedang menunggu pengaktifan daripada pentadbir sistem.', + 201 + ); } } diff --git a/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php b/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php deleted file mode 100644 index 2dd14a8..0000000 --- a/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php +++ /dev/null @@ -1,38 +0,0 @@ -mailMessage( - subject: 'Pengesahan E-mel - Kod OTP', - view: 'auth::emails.verification-otp', - data: [ - 'name' => $notifiable->name, - 'otp' => $this->otp, - 'minutes' => $minutes, - ], - ); - } -} diff --git a/be/Modules/Auth/Entities/EmailVerificationOtp.php b/be/Modules/Auth/Entities/EmailVerificationOtp.php deleted file mode 100644 index 62d4780..0000000 --- a/be/Modules/Auth/Entities/EmailVerificationOtp.php +++ /dev/null @@ -1,44 +0,0 @@ - 'datetime', - 'attempts' => 'integer', - ]; - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - - public function isExpired(): bool - { - return $this->expires_at->isPast(); - } - - public function hasExceededMaxAttempts(): bool - { - return $this->attempts >= (int) config('auth.email_verification.max_attempts', 5); - } -} diff --git a/be/Modules/Auth/Entities/User.php b/be/Modules/Auth/Entities/User.php index f1b61c9..8819330 100644 --- a/be/Modules/Auth/Entities/User.php +++ b/be/Modules/Auth/Entities/User.php @@ -2,7 +2,8 @@ namespace Modules\Auth\Entities; -// use Illuminate\Contracts\Auth\MustVerifyEmail; +use Illuminate\Auth\MustVerifyEmail as MustVerifyEmailTrait; +use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; @@ -22,12 +23,13 @@ use Modules\Role\Entities\Role; use Modules\User\Entities\Address; use Modules\User\Entities\BankDetail; use Modules\User\Entities\Employment; +use Modules\Auth\Notifications\VerifyEmailNotification; use Modules\User\Entities\Heir; -class User extends Authenticatable +class User extends Authenticatable implements MustVerifyEmail { /** @use HasFactory<\Modules\Auth\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, Notifiable, SoftDeletes; + use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, MustVerifyEmailTrait, Notifiable, SoftDeletes; protected $table = 'users'; @@ -301,9 +303,9 @@ class User extends Authenticatable return $currentUser->hasPermissionTo('menyamar pengguna'); } - public function hasVerifiedEmail(): bool + public function sendEmailVerificationNotification(): void { - return $this->email_verified_at !== null; + $this->notify(new VerifyEmailNotification); } /** @@ -315,15 +317,11 @@ class User extends Authenticatable } /** - * Whether credentials are valid for issuing a session (includes verified pending users). + * Whether credentials are valid for issuing a session (includes pending users awaiting admin activation). */ public function canAuthenticate(): bool { - if ($this->canLogin()) { - return true; - } - - return $this->hasVerifiedEmail() && $this->status === 'pending'; + return in_array($this->status, ['active', 'pending'], true); } /** diff --git a/be/Modules/Auth/Http/Controllers/EmailVerificationController.php b/be/Modules/Auth/Http/Controllers/EmailVerificationController.php index ead22a3..dadf6d2 100644 --- a/be/Modules/Auth/Http/Controllers/EmailVerificationController.php +++ b/be/Modules/Auth/Http/Controllers/EmailVerificationController.php @@ -3,60 +3,66 @@ namespace Modules\Auth\Http\Controllers; use App\Http\Controllers\Controller; +use Illuminate\Auth\Events\Verified; use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\URL; use Modules\Auth\Entities\User; -use Modules\Auth\Services\AuthSessionService; -use Modules\Auth\Services\EmailVerificationOtpService; class EmailVerificationController extends Controller { - public function __construct( - protected EmailVerificationOtpService $otpService, - protected AuthSessionService $authSession - ) {} - - public function verify(Request $request): JsonResponse + public function verify(Request $request, string $id, string $hash): RedirectResponse { - $validated = $request->validate([ - 'email' => ['required', 'string', 'email'], - 'otp' => ['required', 'string', 'digits:6'], - ]); - - $user = User::where('email', $validated['email'])->first(); - - if (! $user) { - return response()->json([ - 'success' => false, - 'message' => 'Kod OTP tidak sah.', - ], 422); + if (! URL::hasValidSignature($request)) { + return $this->redirectToFrontend('invalid'); } - $this->otpService->verify($user, $validated['otp']); + $user = User::find($id); - $user->refresh(); + if (! $user || ! hash_equals($hash, sha1($user->getEmailForVerification()))) { + return $this->redirectToFrontend('invalid'); + } - return $this->authSession->createAuthResponse( - $user, - 'E-mel anda telah berjaya disahkan. Akaun anda sedang menunggu pengaktifan daripada pentadbir sistem.' - ); + if ($user->hasVerifiedEmail()) { + return $this->redirectToFrontend('already'); + } + + $user->markEmailAsVerified(); + + event(new Verified($user)); + + return $this->redirectToFrontend('success', $user); } - public function resend(Request $request): JsonResponse + public function send(Request $request): JsonResponse { - $validated = $request->validate([ - 'email' => ['required', 'string', 'email'], - ]); + $user = $request->user(); - $user = User::where('email', $validated['email'])->first(); - - if ($user && ! $user->hasVerifiedEmail()) { - $this->otpService->send($user); + if ($user->hasVerifiedEmail()) { + return response()->json([ + 'success' => true, + 'message' => 'E-mel anda telah disahkan.', + ]); } + $user->sendEmailVerificationNotification(); + return response()->json([ 'success' => true, - 'message' => 'Jika e-mel wujud dan belum disahkan, kod OTP baharu telah dihantar.', + 'message' => 'Pautan pengesahan e-mel telah dihantar.', ]); } + + protected function redirectToFrontend(string $status, ?User $user = null): RedirectResponse + { + $baseUrl = rtrim(config('user.frontend_url'), '/'); + $path = config('auth.verification.frontend_redirect_path', '/profile'); + + if ($user?->status === 'pending') { + $path = '/register?registered=success'; + } + + return redirect()->away("{$baseUrl}{$path}?verified={$status}"); + } } diff --git a/be/Modules/Auth/Notifications/VerifyEmailNotification.php b/be/Modules/Auth/Notifications/VerifyEmailNotification.php new file mode 100644 index 0000000..1506daa --- /dev/null +++ b/be/Modules/Auth/Notifications/VerifyEmailNotification.php @@ -0,0 +1,51 @@ +mailMessage( + subject: 'Pengesahan E-mel - MyKOPKB', + view: 'auth::emails.verify-email', + data: [ + 'name' => $notifiable->name, + 'verificationUrl' => $this->verificationUrl($notifiable), + 'expireMinutes' => $expireMinutes, + ], + ); + } + + protected function verificationUrl(User $notifiable): string + { + $expireMinutes = (int) config('auth.verification.expire_minutes', 60); + + return URL::temporarySignedRoute( + 'verification.verify', + Carbon::now()->addMinutes($expireMinutes), + [ + 'id' => $notifiable->getKey(), + 'hash' => sha1($notifiable->getEmailForVerification()), + ] + ); + } +} diff --git a/be/Modules/Auth/Routes/api.php b/be/Modules/Auth/Routes/api.php index ea3eb18..9a695f6 100644 --- a/be/Modules/Auth/Routes/api.php +++ b/be/Modules/Auth/Routes/api.php @@ -11,10 +11,9 @@ use Laravel\Fortify\Http\Controllers\RegisteredUserController; Route::post('/register', [RegisteredUserController::class, 'store']); Route::post('/login', [AuthenticatedSessionController::class, 'store'])->middleware('block.api.tools'); -Route::post('/verify-email', [EmailVerificationController::class, 'verify']) - ->middleware('throttle:email-verification'); -Route::post('/verify-email/resend', [EmailVerificationController::class, 'resend']) - ->middleware('throttle:email-verification-resend'); +Route::get('/email/verify/{id}/{hash}', [EmailVerificationController::class, 'verify']) + ->middleware('throttle:email-verification') + ->name('verification.verify'); Route::post('/forgot-password', [PasswordResetController::class, 'requestOtp']) ->middleware('throttle:password-reset-request'); @@ -30,4 +29,8 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () { // logout Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']); + + // optional email verification (post-login) + Route::post('/email/verification-notification', [EmailVerificationController::class, 'send']) + ->middleware('throttle:email-verification-resend'); }); \ No newline at end of file diff --git a/be/Modules/Auth/Services/EmailVerificationOtpService.php b/be/Modules/Auth/Services/EmailVerificationOtpService.php deleted file mode 100644 index ed823d7..0000000 --- a/be/Modules/Auth/Services/EmailVerificationOtpService.php +++ /dev/null @@ -1,98 +0,0 @@ -hasVerifiedEmail()) { - return; - } - - $otp = $this->generateOtp(); - - EmailVerificationOtp::query() - ->where('user_id', $user->id) - ->delete(); - - EmailVerificationOtp::create([ - 'user_id' => $user->id, - 'code' => Hash::make($otp), - 'expires_at' => now()->addMinutes($this->expiryMinutes()), - 'attempts' => 0, - ]); - - $user->notify(new EmailVerificationOtpEmail($otp)); - } - - public function verify(User $user, string $otp): void - { - if ($user->hasVerifiedEmail()) { - throw ValidationException::withMessages([ - 'email' => ['E-mel anda telah disahkan.'], - ]); - } - - $record = EmailVerificationOtp::query() - ->where('user_id', $user->id) - ->latest() - ->first(); - - if (! $record) { - throw ValidationException::withMessages([ - 'otp' => ['Kod OTP tidak dijumpai. Sila minta kod baharu.'], - ]); - } - - if ($record->isExpired()) { - $record->delete(); - - throw ValidationException::withMessages([ - 'otp' => ['Kod OTP telah tamat tempoh. Sila minta kod baharu.'], - ]); - } - - if ($record->hasExceededMaxAttempts()) { - $record->delete(); - - throw ValidationException::withMessages([ - 'otp' => ['Terlalu banyak percubaan. Sila minta kod baharu.'], - ]); - } - - if (! Hash::check($otp, $record->code)) { - $record->increment('attempts'); - - if ($record->fresh()->hasExceededMaxAttempts()) { - $record->delete(); - } - - throw ValidationException::withMessages([ - 'otp' => ['Kod OTP tidak sah.'], - ]); - } - - $user->forceFill(['email_verified_at' => now()])->save(); - - EmailVerificationOtp::query() - ->where('user_id', $user->id) - ->delete(); - } - - protected function generateOtp(): string - { - return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); - } - - protected function expiryMinutes(): int - { - return (int) config('auth.email_verification.expiry_minutes', 10); - } -} diff --git a/be/Modules/Auth/resources/views/emails/verification-otp.blade.php b/be/Modules/Auth/resources/views/emails/verification-otp.blade.php deleted file mode 100644 index dcaf385..0000000 --- a/be/Modules/Auth/resources/views/emails/verification-otp.blade.php +++ /dev/null @@ -1,21 +0,0 @@ -@component('mail::message') -@include('emails.partials.header') - -# Pengesahan E-mel - -Assalamualaikum **{{ $name }}**, - -Terima kasih kerana mendaftar. Gunakan kod OTP di bawah untuk mengesahkan alamat e-mel anda. - -@component('mail::panel') -
- {{ $otp }} -
-@endcomponent - -Kod ini akan tamat tempoh dalam **{{ $minutes }} minit**. - -Jika anda tidak membuat pendaftaran ini, abaikan e-mel ini. - -@include('emails.partials.footer') -@endcomponent diff --git a/be/Modules/Auth/resources/views/emails/verify-email.blade.php b/be/Modules/Auth/resources/views/emails/verify-email.blade.php new file mode 100644 index 0000000..e48bedb --- /dev/null +++ b/be/Modules/Auth/resources/views/emails/verify-email.blade.php @@ -0,0 +1,19 @@ +@component('mail::message') +@include('emails.partials.header') + +# Pengesahan E-mel + +Assalamualaikum **{{ $name }}**, + +Terima kasih kerana mendaftar dengan MyKOPKB. Sila sahkan alamat e-mel anda dengan mengklik butang di bawah. + +@component('mail::button', ['url' => $verificationUrl]) +Sahkan E-mel +@endcomponent + +Pautan ini akan tamat tempoh dalam **{{ $expireMinutes }} minit**. + +Jika anda tidak membuat permintaan ini, abaikan e-mel ini. + +@include('emails.partials.footer') +@endcomponent diff --git a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php index 29264a8..b44c3d8 100644 --- a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php +++ b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php @@ -14,7 +14,6 @@ use Illuminate\Support\Facades\Notification; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; use Modules\Auth\Entities\User; -use Modules\Auth\Services\EmailVerificationOtpService; use Modules\MembershipApplication\Entities\MembershipApplication; use Modules\MembershipApplication\Enums\ApplicationStatus; use Modules\MembershipApplication\Enums\BoardDecision; @@ -513,8 +512,6 @@ class MembershipApplicationService ]); } - app(EmailVerificationOtpService::class)->send($user); - return [ 'user' => $user, 'plainPassword' => $plainPassword, diff --git a/be/app/Providers/FortifyServiceProvider.php b/be/app/Providers/FortifyServiceProvider.php index 0077392..9f28f00 100644 --- a/be/app/Providers/FortifyServiceProvider.php +++ b/be/app/Providers/FortifyServiceProvider.php @@ -57,9 +57,9 @@ class FortifyServiceProvider extends ServiceProvider }); RateLimiter::for('email-verification', function (Request $request) { - $throttleKey = Str::transliterate(Str::lower($request->input('email', '')).'|'.$request->ip()); + $throttleKey = Str::transliterate($request->route('id', '').'|'.$request->ip()); - return Limit::perMinute(5)->by($throttleKey); + return Limit::perMinute(6)->by($throttleKey); }); RateLimiter::for('email-verification-resend', function (Request $request) { @@ -87,10 +87,6 @@ class FortifyServiceProvider extends ServiceProvider $shouldBypassPassword = config('app.env', 'local'); if ($user && ($shouldBypassPassword || Hash::check($request->password, $user->password))) { - if (! $user->hasVerifiedEmail()) { - return $user; - } - if (! $user->canAuthenticate()) { throw ValidationException::withMessages([ 'email' => [$user->getLoginRestrictionMessage()], diff --git a/be/config/auth.php b/be/config/auth.php index 59abb17..ddf68b2 100644 --- a/be/config/auth.php +++ b/be/config/auth.php @@ -113,13 +113,13 @@ return [ /* |-------------------------------------------------------------------------- - | Email Verification OTP + | Email Verification Link |-------------------------------------------------------------------------- */ - 'email_verification' => [ - 'expiry_minutes' => (int) env('EMAIL_VERIFICATION_OTP_EXPIRY_MINUTES', 10), - 'max_attempts' => (int) env('EMAIL_VERIFICATION_OTP_MAX_ATTEMPTS', 5), + 'verification' => [ + 'expire_minutes' => (int) env('EMAIL_VERIFICATION_EXPIRE_MINUTES', 60), + 'frontend_redirect_path' => env('EMAIL_VERIFICATION_REDIRECT_PATH', '/profile'), ], /* diff --git a/be/database/migrations/2026_06_04_000001_create_email_verification_otps_table.php b/be/database/migrations/2026_07_12_000001_drop_email_verification_otps_table.php similarity index 83% rename from be/database/migrations/2026_06_04_000001_create_email_verification_otps_table.php rename to be/database/migrations/2026_07_12_000001_drop_email_verification_otps_table.php index b514553..aaabf07 100644 --- a/be/database/migrations/2026_06_04_000001_create_email_verification_otps_table.php +++ b/be/database/migrations/2026_07_12_000001_drop_email_verification_otps_table.php @@ -6,10 +6,12 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { - /** - * Run the migrations. - */ public function up(): void + { + Schema::dropIfExists('email_verification_otps'); + } + + public function down(): void { Schema::create('email_verification_otps', function (Blueprint $table) { $table->uuid('id')->primary(); @@ -18,16 +20,6 @@ return new class extends Migration $table->timestamp('expires_at'); $table->unsignedTinyInteger('attempts')->default(0); $table->timestamps(); - - $table->index(['user_id', 'expires_at']); }); } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('email_verification_otps'); - } }; diff --git a/fe/src/modules/auth/components/EmailVerificationBanner.vue b/fe/src/modules/auth/components/EmailVerificationBanner.vue new file mode 100644 index 0000000..8e10740 --- /dev/null +++ b/fe/src/modules/auth/components/EmailVerificationBanner.vue @@ -0,0 +1,46 @@ + + + diff --git a/fe/src/modules/auth/index.ts b/fe/src/modules/auth/index.ts index 7174ddf..fab7ba7 100644 --- a/fe/src/modules/auth/index.ts +++ b/fe/src/modules/auth/index.ts @@ -4,28 +4,25 @@ export { login, logout, register, - verifyEmail, - resendVerificationEmail, + sendVerificationEmail, requestForgotPassword, resetPassword, fetchCurrentUser, getAuthErrorMessage, getRegisterErrorMessage, - getVerifyEmailErrorMessage, getForgotPasswordErrorMessage, getResetPasswordErrorMessage, resolvePostLoginRoute, resolvePostAuthRoute, isAccountPending, - isLoginVerificationRequired, + isEmailVerified, } from './services/auth.service' export type { LoginCredentials, LoginResponse, RegisterCredentials, RegisterResponse, - VerifyEmailPayload, - VerifyEmailResponse, + ResendVerificationResponse, ForgotPasswordPayload, ForgotPasswordResponse, ResetPasswordPayload, diff --git a/fe/src/modules/auth/pages/AccountPending.vue b/fe/src/modules/auth/pages/AccountPending.vue index 9d0cdaf..0919a65 100644 --- a/fe/src/modules/auth/pages/AccountPending.vue +++ b/fe/src/modules/auth/pages/AccountPending.vue @@ -1,6 +1,6 @@ - - diff --git a/fe/src/modules/auth/routes.ts b/fe/src/modules/auth/routes.ts index 31906f2..5387b1b 100644 --- a/fe/src/modules/auth/routes.ts +++ b/fe/src/modules/auth/routes.ts @@ -13,12 +13,6 @@ export const authPublicRoutes: RouteRecordRaw[] = [ component: () => import('./pages/Register.vue'), meta: { module: 'auth' }, }, - { - path: '/verify-email', - name: 'verify-email', - component: () => import('./pages/VerifyEmail.vue'), - meta: { module: 'auth' }, - }, { path: '/forgot-password', name: 'forgot-password', diff --git a/fe/src/modules/auth/services/auth.service.ts b/fe/src/modules/auth/services/auth.service.ts index 06391b2..3cccecb 100644 --- a/fe/src/modules/auth/services/auth.service.ts +++ b/fe/src/modules/auth/services/auth.service.ts @@ -5,16 +5,13 @@ import type { ForgotPasswordResponse, LoginCredentials, LoginResponse, - LoginVerificationRequiredData, RegisterCredentials, RegisterResponse, - ResendVerificationResponse, ResetPasswordPayload, ResetPasswordResponse, + ResendVerificationResponse, SessionResponse, SwitchRoleResponse, - VerifyEmailPayload, - VerifyEmailResponse, } from '../types/auth.types' export async function login(credentials: LoginCredentials): Promise { @@ -27,13 +24,8 @@ export async function register(credentials: RegisterCredentials): Promise { - const { data } = await api.post('/verify-email', payload) - return data -} - -export async function resendVerificationEmail(email: string): Promise { - const { data } = await api.post('/verify-email/resend', { email }) +export async function sendVerificationEmail(): Promise { + const { data } = await api.post('/v1/email/verification-notification') return data } @@ -73,10 +65,6 @@ export function getRegisterErrorMessage(error: unknown): string { return getApiErrorMessage(error, 'Registration failed. Please try again.') } -export function getVerifyEmailErrorMessage(error: unknown): string { - return getApiErrorMessage(error, 'Email verification failed. Please try again.') -} - export function getForgotPasswordErrorMessage(error: unknown): string { return getApiErrorMessage(error, 'Gagal menghantar kod OTP. Sila cuba lagi.') } @@ -85,15 +73,12 @@ export function getResetPasswordErrorMessage(error: unknown): string { return getApiErrorMessage(error, 'Gagal menetapkan semula kata laluan. Sila cuba lagi.') } -export function isAccountPending(user: { status: string } | null | undefined): boolean { - return user?.status === 'pending' +export function isEmailVerified(user: { email_verified_at?: string | null } | null | undefined): boolean { + return Boolean(user?.email_verified_at) } -export function isLoginVerificationRequired( - response: LoginResponse, -): response is LoginResponse & { data: LoginVerificationRequiredData } { - return 'requires_email_verification' in response.data - && response.data.requires_email_verification === true +export function isAccountPending(user: { status: string } | null | undefined): boolean { + return user?.status === 'pending' } export function resolvePostAuthRoute( diff --git a/fe/src/modules/auth/types/auth.types.ts b/fe/src/modules/auth/types/auth.types.ts index 5a46f42..fad34db 100644 --- a/fe/src/modules/auth/types/auth.types.ts +++ b/fe/src/modules/auth/types/auth.types.ts @@ -45,6 +45,7 @@ export interface AuthUser { birth_date: string | null birth_place: string | null onboarding_completed_at: string | null + email_verified_at: string | null roles?: Array } @@ -54,20 +55,17 @@ export interface LoginSessionData { expires_at: string } -export interface LoginVerificationRequiredData { - email: string - requires_email_verification: true -} - -export interface LoginResponse { +export interface AuthSessionResponse { success: boolean message: string - data: LoginSessionData | LoginVerificationRequiredData + data: LoginSessionData active_role?: AuthRole | null can_switch_role?: boolean redirect_path?: string } +export type LoginResponse = AuthSessionResponse + export interface SwitchRoleResponse extends SessionResponse { message: string } @@ -80,21 +78,7 @@ export interface RegisterCredentials { password_confirmation: string } -export interface RegisterResponse { - success: boolean - message: string - data: { - email: string - requires_email_verification: boolean - } -} - -export interface VerifyEmailPayload { - email: string - otp: string -} - -export type VerifyEmailResponse = LoginResponse +export type RegisterResponse = AuthSessionResponse export interface ResendVerificationResponse { success: boolean diff --git a/fe/src/router/index.ts b/fe/src/router/index.ts index 639454f..c512b52 100644 --- a/fe/src/router/index.ts +++ b/fe/src/router/index.ts @@ -38,7 +38,7 @@ const router = createRouter({ ], }) -const PUBLIC_ROUTE_NAMES = new Set(['login', 'register', 'verify-email', 'membership-application-apply']) +const PUBLIC_ROUTE_NAMES = new Set(['login', 'register', 'membership-application-apply']) router.beforeEach(async (to) => { const authStore = useAuthStore(pinia) @@ -66,7 +66,6 @@ router.beforeEach(async (to) => { return true } - // Redirect authenticated users away from login/register/verify-email. if ( authStore.isAuthenticated && PUBLIC_ROUTE_NAMES.has(routeName) && diff --git a/fe/src/stores/auth.ts b/fe/src/stores/auth.ts index 5e241c4..2e27ff8 100644 --- a/fe/src/stores/auth.ts +++ b/fe/src/stores/auth.ts @@ -36,6 +36,7 @@ export const useAuthStore = defineStore('auth', { isAuthenticated: (state) => state.user !== null, isAccountPending: (state) => state.user?.status === 'pending', isAccountActive: (state) => state.user?.status === 'active', + isEmailVerified: (state) => Boolean(state.user?.email_verified_at), roles: (state) => state.user?.roles ?? [], }, diff --git a/fe/src/themes/Enigma/SideMenu/SideMenu.vue b/fe/src/themes/Enigma/SideMenu/SideMenu.vue index 21dd164..ab02773 100644 --- a/fe/src/themes/Enigma/SideMenu/SideMenu.vue +++ b/fe/src/themes/Enigma/SideMenu/SideMenu.vue @@ -1,4 +1,5 @@