Compare commits

..

8 Commits

Author SHA1 Message Date
Douglas Barone
34206c66fd Refactor loading value assignment in handleSubmit function 2023-12-18 15:14:48 -04:00
Douglas Barone
e4dc910865 Refactor password update functionality 2023-12-18 15:09:36 -04:00
Douglas Barone
e6381d30c4 Update MainForm and Home views 2023-12-18 13:55:03 -04:00
Douglas Barone
09edf2b7c7 Update App.vue and Home.vue 2023-12-18 13:18:06 -04:00
Douglas Barone
d28ab582fa Update favicon and page title 2023-12-18 13:18:03 -04:00
Douglas Barone
17a1622ee9 Remove console.log statements and improve error handling 2023-12-18 10:50:57 -04:00
Douglas Barone
7161cf39f3 Refactor password encoding and updatePassword function 2023-12-18 10:45:23 -04:00
Douglas Barone
8ce77400d6 Refactor encodePassword function to improve password encoding 2023-12-15 17:25:18 -04:00
13 changed files with 282 additions and 127 deletions

View File

@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="./src/web/assets/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vuetify 3</title> <title>Trocar minha senha</title>
</head> </head>
<body> <body>

View File

@ -1,11 +1,14 @@
export function encodePassword(password: string): string { function encodePassword(password: string): string {
let newPassword = '' let encodedPassword = ''
password = '"' + password + '"' password = '"' + password + '"'
for (let i = 0; i < password.length; i++) {
newPassword += String.fromCharCode( for (let i = 0; i < password.length; i++)
encodedPassword += String.fromCharCode(
password.charCodeAt(i) & 0xff, password.charCodeAt(i) & 0xff,
(password.charCodeAt(i) >>> 8) & 0xff (password.charCodeAt(i) >>> 8) & 0xff
) )
}
return newPassword return encodedPassword
} }
export { encodePassword }

View File

@ -8,26 +8,28 @@ import {
import { encodePassword } from './encodePassword' import { encodePassword } from './encodePassword'
const ldapClient = new Client({ const ldapClient = new Client({
url: process.env.LDAP_URL || 'ldap://10.1.0.16' url: process.env.AD_URL || 'ldaps://10.1.0.16',
tlsOptions: {
requestCert: true
}
}) })
const bindUser = process.env.AD_BIND_USER || '' const adminUser = process.env.AD_BIND_USER || ''
const bindPassword = process.env.AD_BIND_PASSWORD || '' const adminPassword = process.env.AD_BIND_PASSWORD || ''
const baseDN = process.env.AD_BASE_DN || '' const baseDN = process.env.AD_BASE_DN || ''
async function getUserDN(username: string): Promise<string> { async function getUserDN(username: string): Promise<string> {
try { try {
await ldapClient.bind(bindUser, bindPassword) await ldapClient.bind(adminUser, adminPassword)
const { searchEntries } = await ldapClient.search(baseDN, { const { searchEntries } = await ldapClient.search(baseDN, {
scope: 'sub',
attributes: ['dn'], attributes: ['dn'],
filter: `(sAMAccountName=${username})` filter: `(sAMAccountName=${username})`
}) })
return searchEntries[0]?.dn return searchEntries[0]?.dn
} catch (err) { } catch (err) {
console.error(err) console.error('Error finding user:', err)
} finally { } finally {
await ldapClient.unbind() await ldapClient.unbind()
} }
@ -37,41 +39,50 @@ async function getUserDN(username: string): Promise<string> {
export async function updatePassword({ export async function updatePassword({
username, username,
password, currentPassword,
newPassword newPassword
}: { }: {
username: string username: string
password: string currentPassword: string
newPassword: string newPassword: string
}): Promise<'SUCCESS' | 'FAIL'> { }): Promise<'SUCCESS' | 'FAIL'> {
try { try {
const userDN = await getUserDN(username) const userDN = await getUserDN(username)
await ldapClient.bind(userDN, password) // Check if user can bind with current password
await ldapClient.bind(userDN, currentPassword)
await ldapClient.unbind()
console.log('binded') // Bind with admin user to change password
await ldapClient.bind(adminUser, adminPassword)
await ldapClient.modify(userDN, [ await ldapClient.modify(userDN, [
new Change({ new Change({
operation: 'delete', operation: 'replace',
modification: new Attribute({
type: 'unicodePwd',
values: [encodePassword(password)]
})
}),
new Change({
operation: 'add',
modification: new Attribute({ modification: new Attribute({
type: 'unicodePwd', type: 'unicodePwd',
values: [encodePassword(newPassword)] values: [encodePassword(newPassword)]
}) })
}) })
// new Change({
// operation: 'delete',
// modification: new Attribute({
// type: 'unicodePwd',
// values: [encodePassword(currentPassword)]
// })
// }),
// new Change({
// operation: 'add',
// modification: new Attribute({
// type: 'unicodePwd',
// values: [encodePassword(newPassword)]
// })
// })
]) ])
return 'SUCCESS' return 'SUCCESS'
} catch (err: any) { } catch (err: any) {
console.log(err)
if (err instanceof InvalidCredentialsError) { if (err instanceof InvalidCredentialsError) {
throw new Error('Usuário ou senha atual incorreta.') throw new Error('Usuário ou senha atual incorreta.')
} }
@ -80,10 +91,11 @@ export async function updatePassword({
throw new Error( throw new Error(
'A senha atual está correta, mas o servidor recusou a alteração. Verifique se a nova senha atende aos requisitos de complexidade.' 'A senha atual está correta, mas o servidor recusou a alteração. Verifique se a nova senha atende aos requisitos de complexidade.'
) )
} else throw err }
console.log('Unexpected error:', err)
throw err
} finally { } finally {
await ldapClient.unbind() await ldapClient.unbind()
console.log('unbinded')
} }
return 'FAIL'
} }

View File

@ -16,19 +16,20 @@ export const appRouter = router({
updatePassword: input( updatePassword: input(
z.object({ z.object({
username: z.string(), username: z.string(),
password: z.string(), currentPassword: z.string(),
newPassword: z.string().min(8) newPassword: z.string().min(8)
}) })
).mutation(async ({ input }) => { ).mutation(async ({ input }) => {
console.log('input', input) const { username, currentPassword, newPassword } = input
const { username, password, newPassword } = input
try { try {
await updatePassword({ await updatePassword({
username, username,
password, currentPassword,
newPassword newPassword
}) })
return 'SUCCESS'
} catch (err: any) { } catch (err: any) {
throw new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'BAD_REQUEST',

View File

@ -1,19 +1,18 @@
<template> <template>
<v-app> <v-app>
<v-main> <v-main>
{{ data }} <router-view />
<v-footer app class="text-grey">
<span class="mr-2">v{{ version }}</span>
<v-spacer />
<span class="mr-2">SERTI-PP</span>
</v-footer>
</v-main> </v-main>
</v-app> </v-app>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import packageInfo from '../../package.json'
import { trpc } from "./trpc"; const version = packageInfo.version
const data = ref<string>("");
onMounted(async () => {
data.value = await trpc.hello.query();
});
</script> </script>

BIN
src/web/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View File

@ -1,6 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 526 B

View File

@ -1,75 +0,0 @@
<template>
<v-container class="fill-height">
<v-responsive class="align-center text-center fill-height">
<v-img height="300" src="@/assets/logo.svg" />
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
<h1 class="text-h2 font-weight-bold">Vuetify</h1>
<div class="py-14" />
<v-row class="d-flex align-center justify-center">
<v-col cols="auto">
<v-btn
href="https://vuetifyjs.com/components/all/"
min-width="164"
rel="noopener noreferrer"
target="_blank"
variant="text"
>
<v-icon
icon="mdi-view-dashboard"
size="large"
start
/>
Components
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn
color="primary"
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
min-width="228"
rel="noopener noreferrer"
size="x-large"
target="_blank"
variant="flat"
>
<v-icon
icon="mdi-speedometer"
size="large"
start
/>
Get Started
</v-btn>
</v-col>
<v-col cols="auto">
<v-btn
href="https://community.vuetifyjs.com/"
min-width="164"
rel="noopener noreferrer"
target="_blank"
variant="text"
>
<v-icon
icon="mdi-account-group"
size="large"
start
/>
Community
</v-btn>
</v-col>
</v-row>
</v-responsive>
</v-container>
</template>
<script setup lang="ts">
//
</script>

View File

@ -0,0 +1,3 @@
<template>
<v-img src="@/assets/logo.png" contain />
</template>

View File

@ -0,0 +1,131 @@
<template>
<v-form ref="form" @submit.prevent="submit" validate-on="input">
<v-card :elevation="2" :loading="loading" :disabled="loading">
<v-card-title class="mb-6 pa-6 text-center">
<span class="headline font-weight-light text-h4">Trocar senha</span>
</v-card-title>
<v-card-text>
<v-text-field
class="mb-4"
v-model="username"
label="Usuário"
:variant="'outlined'"
autocomplete="username"
hint="SIAPE para servidores, CPF para alunos."
prepend-inner-icon="mdi-account"
:rules="[v => !!v || 'O usuário é obrigatório']"
required
density="compact"
/>
<v-text-field
class="mb-4"
v-model="password"
label="Senha atual"
:type="showCurrent ? 'text' : 'password'"
hint="Sua senha atual"
:variant="'outlined'"
prepend-inner-icon="mdi-form-textbox-password"
:append-inner-icon="showCurrent ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="showCurrent = !showCurrent"
:rules="[v => !!v || 'A senha atual é obrigatória']"
required
density="compact"
/>
<v-text-field
class="mb-4"
v-model="newPassword"
label="Nova senha"
:type="showNew ? 'text' : 'password'"
hint="A nova senha que você deseja usar"
:variant="'outlined'"
prepend-inner-icon="mdi-lock-check"
:append-inner-icon="showNew ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="showNew = !showNew"
:rules="newPasswordRules"
required
density="compact"
/>
<v-text-field
v-model="confirmPassword"
label="Confirme a nova senha"
:type="showConfirm ? 'text' : 'password'"
hint="Digite a nova senha mais uma vez"
:variant="'outlined'"
prepend-inner-icon="mdi-lock-check"
:append-inner-icon="showConfirm ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="showConfirm = !showConfirm"
:rules="[
v => !!v || 'A confirmação da senha é obrigatória',
v => v === newPassword || 'As senhas não coincidem'
]"
required
density="compact"
/>
<password-checker class="mt-4" :password="newPassword" />
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
type="submit"
color="primary"
variant="flat"
size="large"
:disabled="!valid || loading"
>
Trocar senha
</v-btn>
</v-card-actions>
</v-card>
</v-form>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import PasswordChecker from './PasswordChecker.vue'
const emit = defineEmits<{
submit: [{ username: string; currentPassword: string; newPassword: string }]
}>()
defineProps<{
loading: boolean
}>()
const username = ref('')
const password = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const showCurrent = ref(false)
const showNew = ref(false)
const showConfirm = ref(false)
// use form refs to validate
const form = ref<HTMLFormElement | null>(null)
const valid = computed(() => {
return form.value?.isValid || false
})
const newPasswordRules = [
(v: string) => !!v || 'A nova senha é obrigatória',
(v: string) =>
/[a-z]/.test(v) || 'A nova senha deve ter pelo menos uma letra minúscula',
(v: string) =>
/[A-Z]/.test(v) || 'A nova senha deve ter pelo menos uma letra maiúscula',
(v: string) =>
v.length >= 8 || 'A nova senha deve ter pelo menos 8 caracteres',
(v: string) =>
/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(v) ||
'A nova senha deve ter pelo menos um caractere especial'
]
async function submit() {
if (await form.value?.validate())
emit('submit', {
username: username.value,
currentPassword: password.value,
newPassword: newPassword.value
})
}
</script>

View File

@ -0,0 +1,52 @@
<template>
<div>
<div class="font-weight-light mb-2">Regras para a nova senha:</div>
<v-alert
v-for="alert in alerts"
:key="alert.text"
:type="alertType(password, alert.rule)"
class="mb-2 rule"
variant="tonal"
density="compact"
:text="alert.text"
/>
</div>
</template>
<script lang="ts" setup>
defineProps<{
password: string
}>()
function alertType(password: string, rule: (password: string) => boolean) {
if (password.length === 0) return 'info'
return rule(password) ? 'success' : 'error'
}
const alerts = [
{
text: 'Uma ou mais letras minúsculas.',
rule: (password: string) => /[a-z]/.test(password)
},
{
text: 'Uma ou mais letras maiúsculas.',
rule: (password: string) => /[A-Z]/.test(password)
},
{
text: '8 ou mais caracteres.',
rule: (password: string) => password.length >= 8
},
{
text: `Um ou mais caracteres especiais (!"#$%&'()*+,\\-./:;<=>?@[]^_\`{}|~).`,
rule: (password: string) =>
/[●!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/.test(password)
}
]
</script>
<style scoped>
.rule {
transition: all 0.2s ease-in-out;
}
</style>

View File

@ -1,7 +1,42 @@
<template> <template>
<HelloWorld /> <v-container>
<v-row justify="center">
<v-col xl="5" lg="6" md="7" sm="10">
<logo class="mx-auto my-3" :style="{ maxWidth: '128px' }" />
<main-form @submit="handleSubmit" :loading="loading" />
</v-col>
</v-row>
</v-container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import HelloWorld from '@/components/HelloWorld.vue' import { ref } from 'vue'
import Logo from '../components/Logo.vue'
import MainForm from '../components/MainForm.vue'
import { trpc } from '../trpc'
const loading = ref(false)
async function handleSubmit({
username,
currentPassword,
newPassword
}: {
username: string
currentPassword: string
newPassword: string
}) {
try {
loading.value = true
await trpc.updatePassword.mutate({
username,
currentPassword,
newPassword
})
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
</script> </script>