Security/register account #1
@@ -0,0 +1,547 @@
|
||||
<template>
|
||||
<Toast :breakpoints="{ '767px': { width: '100%', right: '0', left: '0' } }" />
|
||||
<div class="reset-page">
|
||||
<div class="reset-backdrop" aria-hidden="true"></div>
|
||||
|
||||
<div class="reset-shell">
|
||||
<section class="reset-brand">
|
||||
<div class="brand-mark">
|
||||
<img src="/img/e-Rahn_Logo_FA-01.png" alt="" class="brand-seal" />
|
||||
<img src="/img/e-rahn-lelong.png" alt="E-Rahn Lelong" class="brand-wordmark" />
|
||||
</div>
|
||||
<h1 class="brand-title">ArrahnBid</h1>
|
||||
<p class="brand-copy">
|
||||
Tetapkan kata laluan baharu untuk akaun anda dan teruskan bidaan dengan selamat.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="reset-panel">
|
||||
<header class="panel-header">
|
||||
<h2>Tukar Kata Laluan</h2>
|
||||
<p v-if="email">Emel: {{ email }}</p>
|
||||
<p v-else-if="isValidating">Sedang mengesahkan pautan...</p>
|
||||
<p v-else-if="!isTokenValid">Pautan tidak sah atau telah tamat tempoh.</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
v-if="isTokenValid"
|
||||
class="reset-form"
|
||||
@submit.prevent="doResetPassword"
|
||||
>
|
||||
<label class="field">
|
||||
<span class="field-label">Kata Laluan Baru</span>
|
||||
<div class="field-control" :class="{ 'is-invalid': passwordError }">
|
||||
<Lock class="field-icon" :size="18" :stroke-width="1.75" />
|
||||
<InputText
|
||||
v-model="password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="Masukkan kata laluan baru"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
minlength="6"
|
||||
/>
|
||||
<button
|
||||
class="reveal-btn"
|
||||
type="button"
|
||||
:aria-label="showPassword ? 'Sembunyikan kata laluan' : 'Tunjuk kata laluan'"
|
||||
@click="showPassword = !showPassword"
|
||||
>
|
||||
<EyeOff v-if="showPassword" :size="18" :stroke-width="1.75" />
|
||||
<Eye v-else :size="18" :stroke-width="1.75" />
|
||||
</button>
|
||||
</div>
|
||||
<InlineMessage v-if="passwordError" class="field-error">
|
||||
{{ passwordError }}
|
||||
</InlineMessage>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="field-label">Ulang Kata Laluan Baru</span>
|
||||
<div class="field-control" :class="{ 'is-invalid': confirmError }">
|
||||
<Lock class="field-icon" :size="18" :stroke-width="1.75" />
|
||||
<InputText
|
||||
v-model="confirmPassword"
|
||||
:type="showConfirmPassword ? 'text' : 'password'"
|
||||
placeholder="Ulang kata laluan baru"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
minlength="6"
|
||||
/>
|
||||
<button
|
||||
class="reveal-btn"
|
||||
type="button"
|
||||
:aria-label="showConfirmPassword ? 'Sembunyikan kata laluan' : 'Tunjuk kata laluan'"
|
||||
@click="showConfirmPassword = !showConfirmPassword"
|
||||
>
|
||||
<EyeOff v-if="showConfirmPassword" :size="18" :stroke-width="1.75" />
|
||||
<Eye v-else :size="18" :stroke-width="1.75" />
|
||||
</button>
|
||||
</div>
|
||||
<InlineMessage v-if="confirmError" class="field-error">
|
||||
{{ confirmError }}
|
||||
</InlineMessage>
|
||||
</label>
|
||||
|
||||
<button class="submit-btn" type="submit" :disabled="isLoading">
|
||||
<span>{{ isLoading ? "Sedang kemaskini..." : "Tukar Kata Laluan" }}</span>
|
||||
<LoaderCircle v-if="isLoading" class="spin" :size="18" :stroke-width="2" />
|
||||
<ArrowRight v-else :size="18" :stroke-width="2" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<router-link class="back-link" :to="{ name: 'login' }">
|
||||
<ChevronLeft :size="16" :stroke-width="2" />
|
||||
<span>Kembali ke Log Masuk</span>
|
||||
</router-link>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import {
|
||||
ArrowRight,
|
||||
ChevronLeft,
|
||||
Eye,
|
||||
EyeOff,
|
||||
LoaderCircle,
|
||||
Lock,
|
||||
} from "@lucide/vue";
|
||||
|
||||
export default {
|
||||
name: "ResetPassword",
|
||||
components: {
|
||||
ArrowRight,
|
||||
ChevronLeft,
|
||||
Eye,
|
||||
EyeOff,
|
||||
LoaderCircle,
|
||||
Lock,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isValidating: true,
|
||||
isTokenValid: false,
|
||||
isLoading: false,
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
passwordError: "",
|
||||
confirmError: "",
|
||||
showPassword: false,
|
||||
showConfirmPassword: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
token() {
|
||||
return this.$route.params.token;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.validateToken();
|
||||
},
|
||||
methods: {
|
||||
validateToken() {
|
||||
this.isValidating = true;
|
||||
axios
|
||||
.get(`/resetPassword/${this.token}`)
|
||||
.then((response) => {
|
||||
if (response.data.success) {
|
||||
this.isTokenValid = true;
|
||||
this.email = response.data.email || "";
|
||||
} else {
|
||||
this.isTokenValid = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.isTokenValid = false;
|
||||
this.$toast.add({
|
||||
severity: "error",
|
||||
summary: "Pautan tidak sah",
|
||||
detail: "Pautan tetapan semula kata laluan tidak sah atau telah tamat tempoh.",
|
||||
life: 8000,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.isValidating = false;
|
||||
});
|
||||
},
|
||||
doResetPassword() {
|
||||
this.passwordError = "";
|
||||
this.confirmError = "";
|
||||
|
||||
if (!this.password || this.password.length < 6) {
|
||||
this.passwordError = "Kata laluan mestilah sekurang-kurangnya 6 aksara.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.password !== this.confirmPassword) {
|
||||
this.confirmError = "Kata laluan tidak sepadan.";
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
axios
|
||||
.post("/resetPassword", {
|
||||
token: this.token,
|
||||
password: this.password,
|
||||
confirm_password: this.confirmPassword,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.data.success) {
|
||||
this.$swal
|
||||
.fire({
|
||||
icon: "success",
|
||||
title: "Katalaluan Dikemaskini!",
|
||||
text: response.data.message || "Sila log masuk dengan kata laluan baharu anda.",
|
||||
confirmButtonColor: "#344871",
|
||||
confirmButtonText: "Log Masuk",
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false,
|
||||
})
|
||||
.then(() => {
|
||||
this.$router.push({ name: "login" });
|
||||
});
|
||||
} else {
|
||||
this.$toast.add({
|
||||
severity: "error",
|
||||
summary: "Gagal",
|
||||
detail: response.data.message || "Tidak dapat kemaskini kata laluan.",
|
||||
life: 8000,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const message =
|
||||
error.response?.data?.message ||
|
||||
"Tidak dapat kemaskini kata laluan. Sila cuba lagi.";
|
||||
this.$toast.add({
|
||||
severity: "error",
|
||||
summary: "Gagal",
|
||||
detail: message,
|
||||
life: 8000,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=Sora:wght@500;600;700&display=swap");
|
||||
|
||||
.reset-page {
|
||||
--ink: #132a4f;
|
||||
--navy: #1e417b;
|
||||
--blue: #2f68c5;
|
||||
--gold: #fec234;
|
||||
--panel: rgba(255, 252, 247, 0.94);
|
||||
--line: rgba(19, 42, 79, 0.12);
|
||||
--muted: rgba(19, 42, 79, 0.68);
|
||||
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
overflow: hidden;
|
||||
font-family: "Manrope", sans-serif;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.reset-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(115deg, rgba(19, 42, 79, 0.78) 0%, rgba(30, 65, 123, 0.55) 42%, rgba(254, 194, 52, 0.35) 100%),
|
||||
url("/src/assets/background-lelong.png") center / cover no-repeat;
|
||||
transform: scale(1.04);
|
||||
animation: backdrop-drift 18s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.reset-shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(980px, 100%);
|
||||
display: grid;
|
||||
grid-template-columns: 1.05fr 0.95fr;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
animation: shell-enter 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.reset-brand {
|
||||
padding: 3rem 2.75rem;
|
||||
color: #fffdf8;
|
||||
background:
|
||||
linear-gradient(160deg, rgba(19, 42, 79, 0.28), rgba(47, 104, 197, 0.1)),
|
||||
radial-gradient(circle at top left, rgba(254, 194, 52, 0.35), transparent 55%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
margin-bottom: auto;
|
||||
animation: rise 0.8s 0.1s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.brand-seal {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
.brand-wordmark {
|
||||
width: min(180px, 50vw);
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
margin: 0 0 0.75rem;
|
||||
font-family: "Sora", sans-serif;
|
||||
font-size: clamp(2.4rem, 4vw, 3.4rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1.05;
|
||||
animation: rise 0.8s 0.18s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
margin: 0;
|
||||
max-width: 30ch;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.55;
|
||||
color: rgba(255, 253, 248, 0.88);
|
||||
animation: rise 0.8s 0.26s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.reset-panel {
|
||||
background: var(--panel);
|
||||
padding: 2.75rem 2.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
animation: rise 0.8s 0.2s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.panel-header h2 {
|
||||
margin: 0 0 0.4rem;
|
||||
font-family: "Sora", sans-serif;
|
||||
font-size: 1.85rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.panel-header p {
|
||||
margin: 0 0 1.75rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.panel-header a,
|
||||
.back-link {
|
||||
color: var(--blue);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.panel-header a:hover,
|
||||
.back-link:hover {
|
||||
color: var(--navy);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.reset-form {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.field-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
min-height: 3.15rem;
|
||||
padding: 0 0.95rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.field-control:focus-within {
|
||||
border-color: var(--blue);
|
||||
box-shadow: 0 0 0 3px rgba(47, 104, 197, 0.14);
|
||||
}
|
||||
|
||||
.field-control.is-invalid {
|
||||
border-color: #d14343;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
flex: 0 0 auto;
|
||||
color: var(--navy);
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.field-control :deep(.p-inputtext) {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0.85rem 0 !important;
|
||||
font-family: inherit;
|
||||
font-size: 0.98rem;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.reveal-btn {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0.2rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--navy);
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.reveal-btn:hover {
|
||||
opacity: 1;
|
||||
background: rgba(30, 65, 123, 0.06);
|
||||
}
|
||||
|
||||
.field-error {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 0.35rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.55rem;
|
||||
width: 100%;
|
||||
min-height: 3.2rem;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, var(--navy) 0%, var(--blue) 100%);
|
||||
color: #fff;
|
||||
font-family: "Sora", sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
filter: brightness(1.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.8;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
margin-top: 1.5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes shell-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(18px) scale(0.985);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(14px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes backdrop-drift {
|
||||
from {
|
||||
transform: scale(1.04) translate3d(0, 0, 0);
|
||||
}
|
||||
to {
|
||||
transform: scale(1.08) translate3d(-1.2%, -0.8%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.reset-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.reset-brand {
|
||||
min-height: auto;
|
||||
padding: 2rem 1.6rem 1.4rem;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.reset-panel {
|
||||
padding: 1.75rem 1.4rem 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div else style="min-height: 70vh">
|
||||
<div v-else style="min-height: 70vh">
|
||||
<div v-if="status">
|
||||
<div
|
||||
class="
|
||||
@@ -104,44 +104,38 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAuthenticationUserStore } from "@/store/modules/authentication";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
data: [],
|
||||
data_2: [],
|
||||
data: {},
|
||||
loading: false,
|
||||
status: false,
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
mounted() {
|
||||
this.profile();
|
||||
},
|
||||
methods: {
|
||||
profile() {
|
||||
this.loading = true;
|
||||
axios
|
||||
.get("/user/profile/" + useAuthenticationUserStore().nokp, {
|
||||
headers: {
|
||||
Authorization: "Bearer " + useAuthenticationUserStore().token,
|
||||
},
|
||||
})
|
||||
.get("/user/profile")
|
||||
.then((response) => {
|
||||
if (response.data.success == true) {
|
||||
this.status = true;
|
||||
this.data = response.data.message[0];
|
||||
this.data_2 = response.data.message[1];
|
||||
this.loading = false;
|
||||
this.data = response.data.message[0] || {};
|
||||
} else {
|
||||
this.status = false;
|
||||
this.loading = false;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(error);
|
||||
this.status = false;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -25,7 +25,8 @@ axios.interceptors.response.use(
|
||||
if (
|
||||
currentName !== "login" &&
|
||||
currentName !== "register" &&
|
||||
currentName !== "forgot-password"
|
||||
currentName !== "forgot-password" &&
|
||||
currentName !== "reset-password"
|
||||
) {
|
||||
router.push({ name: "login" });
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ import ThePay from "../components/pay/ThePay.vue";
|
||||
import ThePayCdm from "../components/pay/ThePayCdm.vue";
|
||||
import ThePolicy from "../components/policy/ThePolicy.vue";
|
||||
import TheForgotPassword from "../components/authentication/TheForgotPassword.vue";
|
||||
import TheResetPassword from "../components/authentication/TheResetPassword.vue";
|
||||
|
||||
import {
|
||||
createRouter,
|
||||
@@ -169,6 +170,12 @@ const routes = [
|
||||
component: TheForgotPassword,
|
||||
meta: { guest: true },
|
||||
},
|
||||
{
|
||||
path: "/reset-password/:token",
|
||||
name: "reset-password",
|
||||
component: TheResetPassword,
|
||||
meta: { guest: true },
|
||||
},
|
||||
{
|
||||
path: "/policy",
|
||||
name: "policy",
|
||||
|
||||
Reference in New Issue
Block a user