Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
dtd
/
civitas.ui
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit fb62f636
authored
Apr 24, 2025
by
Samuel Taniel Mulyadi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add token expired
1 parent
d7205995
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
254 additions
and
107 deletions
@core/scss/template/pages/page-auth.scss
@core/stores/keycloakStore.ts
layouts/components/DefaultLayoutWithHorizontalNav.vue
layouts/components/DefaultLayoutWithVerticalNav.vue
layouts/components/NavbarTokenExpiredTime.vue
plugins/keycloak.ts
views/dstipro/beranda/keamanan/index.vue
@core/scss/template/pages/page-auth.scss
View file @
fb62f63
...
...
@@ -75,7 +75,7 @@
min-block-size
:
100vh
;
/* Ensures the element covers the full viewport height */
padding-block-start
:
10px
;
// margin: 5% 5%;marginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmargin
// margin: 5% 5%;marginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmargin
margin
}
.main-component-margin
{
...
...
@core/stores/keycloakStore.ts
View file @
fb62f63
...
...
@@ -13,6 +13,7 @@ const username = ref<string>('')
const
civitas
=
ref
<
string
>
(
''
)
const
kodeIdentitas
=
ref
<
string
>
(
''
)
const
accessToken
=
ref
<
string
>
(
''
)
const
tokenParsedExp
=
ref
<
number
>
(
0
)
const
refreshTokenExp
=
ref
<
number
>
(
0
)
const
roles
=
ref
<
string
[]
>
([])
const
selectedRole
=
useStorage
(
'selectedRole'
,
'admin'
)
...
...
@@ -28,6 +29,7 @@ const refresh = (): void => {
username
.
value
=
tokenParsed
.
preferred_username
||
''
civitas
.
value
=
tokenParsed
.
civitas
||
''
kodeIdentitas
.
value
=
tokenParsed
.
kodeIdentitas
||
''
tokenParsedExp
.
value
=
tokenParsed
.
exp
||
0
accessToken
.
value
=
keycloakInstance
.
token
||
''
refreshTokenExp
.
value
=
refreshedTokenParsed
.
exp
||
0
roles
.
value
=
keycloakInstance
.
resourceAccess
?.
vueplayground
?.
roles
??
[]
...
...
layouts/components/DefaultLayoutWithHorizontalNav.vue
View file @
fb62f63
<
script
lang=
"ts"
setup
>
import
{
HorizontalNavLayout
}
from
'@layouts'
import
navItems
from
'@/navigation/horizontal'
import
{
computed
,
onMounted
,
onUnmounted
,
ref
}
from
'vue'
import
{
useKeycloakStore
}
from
'@/@core/stores/keycloakStore'
import
keycloakInstance
from
'@/keycloak'
// Components
import
Footer
from
'@/layouts/components/Footer.vue'
import
NavbarThemeSwitcher
from
'@/layouts/components/NavbarThemeSwitcher.vue'
import
NavbarTokenExpiredTime
from
'@/layouts/components/NavbarTokenExpiredTime.vue'
import
UserProfile
from
'@/layouts/components/UserProfile.vue'
import
navItems
from
'@/navigation/horizontal'
// Keycloak & state
const
keycloakStore
=
useKeycloakStore
()
const
authenticated
=
computed
(()
=>
keycloakStore
.
authenticated
)
const
now
=
ref
(
Math
.
floor
(
Date
.
now
()
/
1000
))
const
tokenLifetime
=
ref
(
0
)
let
timer
:
ReturnType
<
typeof
setInterval
>
onMounted
(()
=>
{
// Set tokenLifetime only once on mount
if
(
keycloakInstance
.
tokenParsed
?.
exp
&&
keycloakInstance
.
tokenParsed
?.
iat
)
tokenLifetime
.
value
=
keycloakInstance
.
tokenParsed
.
exp
-
keycloakInstance
.
tokenParsed
.
iat
// Start timer
timer
=
setInterval
(()
=>
{
now
.
value
=
Math
.
floor
(
Date
.
now
()
/
1000
)
},
1000
)
})
onUnmounted
(()
=>
{
clearInterval
(
timer
)
})
// Computed expiration values
const
computedExpIn
=
computed
(()
=>
{
return
authenticated
.
value
&&
keycloakInstance
.
tokenParsed
?.
exp
?
Math
.
max
(
keycloakInstance
.
tokenParsed
?.
exp
-
now
.
value
,
0
)
:
0
})
</
script
>
<
template
>
...
...
@@ -39,10 +73,27 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
:languages=
"themeConfig.app.i18n.langConfig"
/>
-->
<NavbarTokenExpiredTime
/>
<NavbarThemeSwitcher
/>
<!--
<NavbarShortcuts
/>
-->
<!--
<NavBarNotifications
class=
"me-2"
/>
-->
<!--
<div
class=
"pa-4"
>
<VAlert
v-if=
"authenticated"
type=
"info"
border=
"start"
variant=
"outlined"
class=
"mb-4"
>
<h3
class=
"text-h6 font-weight-bold"
>
Access token expires in
{{
computedExpIn
}}
sec,
Refresh token in
{{
computedRefExpIn
}}
sec,
Session:
{{
computedSessionTimer
}}
</h3>
</VAlert>
</div>
-->
<UserProfile
/>
</
template
>
...
...
layouts/components/DefaultLayoutWithVerticalNav.vue
View file @
fb62f63
...
...
@@ -5,6 +5,7 @@ import navItems from '@/navigation/vertical'
// Components
import
Footer
from
'@/layouts/components/Footer.vue'
import
NavbarThemeSwitcher
from
'@/layouts/components/NavbarThemeSwitcher.vue'
import
NavbarTokenExpiredTime
from
'@/layouts/components/NavbarTokenExpiredTime.vue'
import
UserProfile
from
'@/layouts/components/UserProfile.vue'
// @layouts plugin
...
...
@@ -41,6 +42,7 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
:languages=
"themeConfig.app.i18n.langConfig"
/>
-->
<NavbarTokenExpiredTime
/>
<NavbarThemeSwitcher
/>
<!--
<NavbarShortcuts
/>
-->
<!--
<NavBarNotifications
class=
"me-2"
/>
-->
...
...
layouts/components/NavbarTokenExpiredTime.vue
0 → 100644
View file @
fb62f63
<
script
setup
lang=
"ts"
>
import
{
computed
,
onMounted
,
onUnmounted
,
ref
}
from
'vue'
import
{
useKeycloakStore
}
from
'@/@core/stores/keycloakStore'
import
keycloakInstance
from
'@/keycloak'
const
keycloakStore
=
useKeycloakStore
()
const
authenticated
=
computed
(()
=>
keycloakStore
.
authenticated
)
const
now
=
ref
(
Math
.
floor
(
Date
.
now
()
/
1000
))
const
tokenLifetime
=
ref
(
0
)
let
timer
:
ReturnType
<
typeof
setInterval
>
onMounted
(()
=>
{
// Set tokenLifetime only once
if
(
keycloakInstance
.
tokenParsed
?.
exp
&&
keycloakInstance
.
tokenParsed
?.
iat
)
tokenLifetime
.
value
=
keycloakInstance
.
tokenParsed
.
exp
-
keycloakInstance
.
tokenParsed
.
iat
timer
=
setInterval
(()
=>
{
now
.
value
=
Math
.
floor
(
Date
.
now
()
/
1000
)
},
1000
)
})
onUnmounted
(()
=>
{
clearInterval
(
timer
)
})
const
computedExpIn
=
computed
(()
=>
{
return
authenticated
.
value
&&
keycloakInstance
.
tokenParsed
?.
exp
?
Math
.
max
(
keycloakInstance
.
tokenParsed
.
exp
-
now
.
value
,
0
)
:
0
})
</
script
>
<
template
>
<div
v-if=
"authenticated"
class=
"me-4"
style=
"position: relative; block-size: 40px; inline-size: 40px;"
>
<VProgressCircular
:model-value=
"(computedExpIn / tokenLifetime) * 100"
:size=
"40"
:width=
"3"
color=
"primary"
/>
<div
class=
"text-subtitle-1 font-weight-bold"
style=
"position: absolute; inset-block-start: 50%; inset-inline-start: 50%; transform: translate(-50%, -50%);"
>
{{
computedExpIn
}}
</div>
</div>
</
template
>
plugins/keycloak.ts
View file @
fb62f63
...
...
@@ -15,6 +15,19 @@ export default defineNuxtPlugin(async nuxtApp => {
keycloakStore
.
refresh
()
console
.
log
(
'User is authenticated'
)
navigateTo
(
'/profile'
)
setInterval
(()
=>
{
const
now
=
Math
.
floor
(
Date
.
now
()
/
1000
)
const
tokenExp
=
keycloakInstance
.
tokenParsed
?.
exp
??
0
if
(
tokenExp
<=
now
)
{
console
.
warn
(
'Token expired. Logging out...'
)
keycloakInstance
.
logout
({
redirectUri
:
`
${
window
.
location
.
origin
}
/login`
})
}
else
{
console
.
log
(
'Token expires in:'
,
tokenExp
-
now
,
'seconds'
)
}
},
10
_000
)
}
else
{
console
.
log
(
'User is not authenticated'
)
...
...
views/dstipro/beranda/keamanan/index.vue
View file @
fb62f63
<
script
lang=
"ts"
setup
>
import
{
useKeycloakStore
}
from
"@core/stores/keycloakStore"
;
import
{
useKeycloakStore
}
from
'@core/stores/keycloakStore'
const
keycloakStore
=
useKeycloakStore
()
;
const
keycloakStore
=
useKeycloakStore
()
const
isCurrentPasswordVisible
=
ref
(
false
)
;
const
isNewPasswordVisible
=
ref
(
false
)
;
const
isConfirmPasswordVisible
=
ref
(
false
)
;
const
currentPassword
=
ref
(
""
);
const
newPassword
=
ref
(
""
);
const
newPasswordError
=
ref
(
""
);
const
confirmPassword
=
ref
(
""
);
const
isSubmitting
=
ref
(
false
)
;
const
isCurrentPasswordVisible
=
ref
(
false
)
const
isNewPasswordVisible
=
ref
(
false
)
const
isConfirmPasswordVisible
=
ref
(
false
)
const
currentPassword
=
ref
(
''
)
const
newPassword
=
ref
(
''
)
const
newPasswordError
=
ref
(
''
)
const
confirmPassword
=
ref
(
''
)
const
isSubmitting
=
ref
(
false
)
watch
(
newPassword
,
(
newValue
)
=>
{
watch
(
newPassword
,
newValue
=>
{
if
(
currentPassword
.
value
&&
newValue
===
currentPassword
.
value
)
{
newPasswordError
.
value
=
"Kata sandi baru tidak boleh sama dengan kata sandi lama."
;
}
else
{
newPasswordError
.
value
=
""
;
newPasswordError
.
value
=
'Kata sandi baru tidak boleh sama dengan kata sandi lama.'
}
});
else
{
newPasswordError
.
value
=
''
}
})
const
passwordRequirements
=
[
"Panjang minimal 8 karakter, maksimal 20 karakter"
,
"Minimal satu karakter huruf besar"
,
"Minimal satu angka"
,
"Minimal satu simbol, atau karakter spasi"
,
]
;
'Panjang minimal 8 karakter, maksimal 20 karakter'
,
'Minimal satu karakter huruf besar'
,
'Minimal satu angka'
,
'Minimal satu simbol, atau karakter spasi'
,
]
// Aturan Validasi
const
oldPasswordRules
=
[
(
v
:
string
)
=>
!!
v
||
"Konfirmasi kata sandi diperlukan"
,
]
;
(
v
:
string
)
=>
!!
v
||
'Konfirmasi kata sandi diperlukan'
,
]
const
passwordRules
=
[
(
v
:
string
)
=>
!!
v
||
"Kata sandi diperlukan"
,
(
v
:
string
)
=>
v
.
length
>=
8
||
"Kata sandi minimal terdiri dari 8 karakter"
,
(
v
:
string
)
=>
!!
v
||
'Kata sandi diperlukan'
,
(
v
:
string
)
=>
v
.
length
>=
8
||
'Kata sandi minimal terdiri dari 8 karakter'
,
(
v
:
string
)
=>
/
[
a-z
]
/
.
test
(
v
)
||
"Kata sandi setidaknya mengandung satu huruf kecil"
,
/
[
a-z
]
/
.
test
(
v
)
||
'Kata sandi setidaknya mengandung satu huruf kecil'
,
(
v
:
string
)
=>
/
[
A-Z
]
/
.
test
(
v
)
||
"Kata sandi setidaknya mengandung satu huruf besar"
,
(
v
:
string
)
=>
/
\d
/
.
test
(
v
)
||
"Kata sandi setidaknya berisi satu angka"
,
/
[
A-Z
]
/
.
test
(
v
)
||
'Kata sandi setidaknya mengandung satu huruf besar'
,
(
v
:
string
)
=>
/
\d
/
.
test
(
v
)
||
'Kata sandi setidaknya berisi satu angka'
,
(
v
:
string
)
=>
/
[
!"#$%&'()*+,-.
/
:;<=>?@[
\\\]
^_`{|}~
]
/
.
test
(
v
)
||
"Kata sandi setidaknya mengandung satu simbol atau spasi"
,
]
;
/
[
!"#$%&'()*+,-.
/
:;<=>?@[
\\\]
^_`{|}~
]
/
.
test
(
v
)
||
'Kata sandi setidaknya mengandung satu simbol atau spasi'
,
]
const
confirmPasswordRules
=
[
(
v
:
string
)
=>
!!
v
||
"Konfirmasi kata sandi diperlukan"
,
(
v
:
string
)
=>
v
===
newPassword
.
value
||
"Kata sandi tidak cocok"
,
]
;
(
v
:
string
)
=>
!!
v
||
'Konfirmasi kata sandi diperlukan'
,
(
v
:
string
)
=>
v
===
newPassword
.
value
||
'Kata sandi tidak cocok'
,
]
// Generate Password
function
generatePassword
(
length
:
number
=
10
):
string
{
const
lowercase
=
"abcdefghijklmnopqrstuvwxyz"
;
const
uppercase
=
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
;
const
numbers
=
"0123456789"
;
const
symbols
=
` !"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`
;
const
lowercase
=
'abcdefghijklmnopqrstuvwxyz'
const
uppercase
=
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const
numbers
=
'0123456789'
const
symbols
=
' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
// At least one character from each required category
const
mandatoryCharacters
=
[
...
...
@@ -64,113 +65,119 @@ function generatePassword(length: number = 10): string {
uppercase
[
Math
.
floor
(
Math
.
random
()
*
uppercase
.
length
)],
numbers
[
Math
.
floor
(
Math
.
random
()
*
numbers
.
length
)],
symbols
[
Math
.
floor
(
Math
.
random
()
*
symbols
.
length
)],
]
;
]
// Combine all character sets into one charset
const
allCharacters
=
lowercase
+
uppercase
+
numbers
+
symbols
;
const
remainingLength
=
length
-
mandatoryCharacters
.
length
;
let
password
=
mandatoryCharacters
;
const
allCharacters
=
lowercase
+
uppercase
+
numbers
+
symbols
const
remainingLength
=
length
-
mandatoryCharacters
.
length
let
password
=
mandatoryCharacters
// Fill the rest of the password with random characters from the combined charset
for
(
let
i
=
0
;
i
<
remainingLength
;
i
++
)
{
const
randomChar
=
allCharacters
[
Math
.
floor
(
Math
.
random
()
*
allCharacters
.
length
)];
password
.
push
(
randomChar
);
const
randomChar
=
allCharacters
[
Math
.
floor
(
Math
.
random
()
*
allCharacters
.
length
)]
password
.
push
(
randomChar
)
}
// Shuffle the password to ensure randomness
password
=
password
.
sort
(()
=>
Math
.
random
()
-
0.5
)
;
password
=
password
.
sort
(()
=>
Math
.
random
()
-
0.5
)
// Return the password as a string
return
password
.
join
(
""
);
return
password
.
join
(
''
)
}
// fungsi untuk encoding Base64
function
toBase64
(
str
:
string
)
{
return
btoa
(
unescape
(
encodeURIComponent
(
str
)))
;
return
btoa
(
unescape
(
encodeURIComponent
(
str
)))
}
// Set Password
async
function
setPassword
()
{
isSubmitting
.
value
=
true
;
isSubmitting
.
value
=
true
if
(
isEmpty
(
currentPassword
.
value
))
{
alert
(
"Kata Sandi Lama tidak boleh kosong"
);
return
;
alert
(
'Kata Sandi Lama tidak boleh kosong'
)
return
}
// Buat objek data dengan password yang sudah di-encoding
const
requestData
=
{
oldPassword
:
toBase64
(
currentPassword
.
value
),
newPassword
:
toBase64
(
newPassword
.
value
),
}
;
}
if
(
newPassword
.
value
.
length
<
8
||
!
/
[
a-z
]
/
.
test
(
newPassword
.
value
)
||
!
/
\d
/
.
test
(
newPassword
.
value
)
||
!
/
[
!"#$%&'()*+,-.
/
:;<=>?@[
\]
^_`{|}~
]
/
.
test
(
newPassword
.
value
)
newPassword
.
value
.
length
<
8
||
!
/
[
a-z
]
/
.
test
(
newPassword
.
value
)
||
!
/
\d
/
.
test
(
newPassword
.
value
)
||
!
/
[
!"#$%&'()*+,-.
/
:;<=>?@[
\]
^_`{|}~
]
/
.
test
(
newPassword
.
value
)
)
{
alert
(
"Kata Sandi harus memiliki minimal 8 karakter, setidaknya satu huruf besar, satu angka, dan satu karakter khusus."
);
return
;
'Kata Sandi harus memiliki minimal 8 karakter, setidaknya satu huruf besar, satu angka, dan satu karakter khusus.'
,
)
return
}
if
(
newPassword
.
value
===
currentPassword
.
value
)
{
alert
(
"Kata sandi baru tidak boleh sama dengan kata sandi lama."
);
return
;
alert
(
'Kata sandi baru tidak boleh sama dengan kata sandi lama.'
)
return
}
if
(
newPassword
.
value
!==
confirmPassword
.
value
)
{
alert
(
"Kata Sandi Baru dan Konfirmasi Kata Sandi tidak cocok."
);
return
;
alert
(
'Kata Sandi Baru dan Konfirmasi Kata Sandi tidak cocok.'
)
return
}
try
{
// Panggil API untuk mengganti password
const
apiEndpoint
=
`https://api.ui.ac.id/my/pw`
;
const
apiEndpoint
=
'https://api.ui.ac.id/my/pw'
const
response
=
await
fetch
(
apiEndpoint
,
{
method
:
"POST"
,
method
:
'POST'
,
headers
:
{
"Content-Type"
:
"application/json"
,
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
'Content-Type'
:
'application/json'
,
'Authorization'
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
},
body
:
JSON
.
stringify
(
requestData
),
})
;
})
// Check for HTTP status code 401 which indicates old password is incorrect
if
(
response
.
status
===
401
)
{
throw
new
Error
(
"Kata sandi lama tidak sesuai"
);
}
if
(
response
.
status
===
401
)
throw
new
Error
(
'Kata sandi lama tidak sesuai'
)
if
(
!
response
.
ok
)
{
const
errorData
=
await
response
.
json
()
;
throw
new
Error
(
errorData
.
message
||
"Gagal mengubah password"
);
const
errorData
=
await
response
.
json
()
throw
new
Error
(
errorData
.
message
||
'Gagal mengubah password'
)
}
// Jika berhasil, tampilkan pesan dan reset form
alert
(
"Kata sandi berhasil diubah. Silakan logout dan login kembali dengan kata sandi baru."
);
currentPassword
.
value
=
""
;
newPassword
.
value
=
""
;
confirmPassword
.
value
=
""
;
}
catch
(
error
)
{
'Kata sandi berhasil diubah. Silakan logout dan login kembali dengan kata sandi baru.'
,
)
currentPassword
.
value
=
''
newPassword
.
value
=
''
confirmPassword
.
value
=
''
}
catch
(
error
)
{
// Handle error
if
(
error
instanceof
Error
)
{
alert
(
error
.
message
)
;
}
else
{
alert
(
"Terjadi kesalahan saat mengubah kata sandi"
);
if
(
error
instanceof
Error
)
alert
(
error
.
message
)
else
alert
(
'Terjadi kesalahan saat mengubah kata sandi'
)
}
}
finally
{
isSubmitting
.
value
=
false
;
finally
{
isSubmitting
.
value
=
false
}
// Reset input
currentPassword
.
value
=
""
;
newPassword
.
value
=
""
;
confirmPassword
.
value
=
""
;
currentPassword
.
value
=
''
newPassword
.
value
=
''
confirmPassword
.
value
=
''
}
</
script
>
...
...
@@ -186,7 +193,10 @@ async function setPassword() {
<VCardText
class=
"pt-0"
>
<!-- 👉 Current Password -->
<VRow>
<VCol
cols=
"12"
md=
"6"
>
<VCol
cols=
"12"
md=
"6"
>
<!-- 👉 current password -->
<VTextField
v-model=
"currentPassword"
...
...
@@ -197,18 +207,21 @@ async function setPassword() {
"
autocomplete=
"on"
label=
"Kata Sandi Lama"
:rules=
"oldPasswordRules"
clearable
@
click:append-inner=
"
isCurrentPasswordVisible = !isCurrentPasswordVisible
"
:rules=
"oldPasswordRules"
clearable
/>
</VCol>
</VRow>
<!-- 👉 New Password -->
<VRow>
<VCol
cols=
"12"
md=
"6"
>
<VCol
cols=
"12"
md=
"6"
>
<!-- 👉 new password -->
<VTextField
v-model=
"newPassword"
...
...
@@ -219,16 +232,19 @@ async function setPassword() {
"
label=
"Kata Sandi Baru"
autocomplete=
"on"
@
click:append-inner=
"
isNewPasswordVisible = !isNewPasswordVisible
"
:rules=
"passwordRules"
:error-messages=
"newPasswordError"
clearable
@
click:append-inner=
"
isNewPasswordVisible = !isNewPasswordVisible
"
/>
</VCol>
<VCol
cols=
"12"
md=
"6"
>
<VCol
cols=
"12"
md=
"6"
>
<!-- 👉 confirm password -->
<VTextField
v-model=
"confirmPassword"
...
...
@@ -239,11 +255,11 @@ async function setPassword() {
"
autocomplete=
"on"
label=
"Konfirmasi Kata Sandi"
:rules=
"confirmPasswordRules"
clearable
@
click:append-inner=
"
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
:rules=
"confirmPasswordRules"
clearable
/>
</VCol>
</VRow>
...
...
@@ -276,11 +292,20 @@ async function setPassword() {
<!-- 👉 Action Buttons -->
<div
class=
"d-flex flex-wrap gap-4"
>
<VBtn
@
click=
"setPassword"
:disabled=
"isSubmitting"
>
{{
<VBtn
:disabled=
"isSubmitting"
@
click=
"setPassword"
>
{{
isSubmitting ? "Sedang Memproses..." : "Simpan Perubahan"
}}
</VBtn>
}}
</VBtn>
<VBtn
type=
"reset"
color=
"secondary"
variant=
"outlined"
>
<VBtn
type=
"reset"
color=
"secondary"
variant=
"outlined"
>
Reset
</VBtn>
<VBtn
...
...
@@ -291,8 +316,8 @@ async function setPassword() {
confirmPassword = newPassword;
"
>
Generate Kata Sandi
</VBtn
>
Generate Kata Sandi
</VBtn
>
</div>
</VCardText>
</VForm>
...
...
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment