Compare commits
8 Commits
50c33eb172
...
34206c66fd
Author | SHA1 | Date | |
---|---|---|---|
|
34206c66fd | ||
|
e4dc910865 | ||
|
e6381d30c4 | ||
|
09edf2b7c7 | ||
|
d28ab582fa | ||
|
17a1622ee9 | ||
|
7161cf39f3 | ||
|
8ce77400d6 |
|
@ -2,9 +2,9 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<title>Vuetify 3</title>
|
||||
<title>Trocar minha senha</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
export function encodePassword(password: string): string {
|
||||
let newPassword = ''
|
||||
function encodePassword(password: string): string {
|
||||
let encodedPassword = ''
|
||||
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) >>> 8) & 0xff
|
||||
)
|
||||
|
||||
return encodedPassword
|
||||
}
|
||||
return newPassword
|
||||
}
|
||||
|
||||
export { encodePassword }
|
||||
|
|
|
@ -8,26 +8,28 @@ import {
|
|||
import { encodePassword } from './encodePassword'
|
||||
|
||||
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 bindPassword = process.env.AD_BIND_PASSWORD || ''
|
||||
const adminUser = process.env.AD_BIND_USER || ''
|
||||
const adminPassword = process.env.AD_BIND_PASSWORD || ''
|
||||
const baseDN = process.env.AD_BASE_DN || ''
|
||||
|
||||
async function getUserDN(username: string): Promise<string> {
|
||||
try {
|
||||
await ldapClient.bind(bindUser, bindPassword)
|
||||
await ldapClient.bind(adminUser, adminPassword)
|
||||
|
||||
const { searchEntries } = await ldapClient.search(baseDN, {
|
||||
scope: 'sub',
|
||||
attributes: ['dn'],
|
||||
filter: `(sAMAccountName=${username})`
|
||||
})
|
||||
|
||||
return searchEntries[0]?.dn
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.error('Error finding user:', err)
|
||||
} finally {
|
||||
await ldapClient.unbind()
|
||||
}
|
||||
|
@ -37,41 +39,50 @@ async function getUserDN(username: string): Promise<string> {
|
|||
|
||||
export async function updatePassword({
|
||||
username,
|
||||
password,
|
||||
currentPassword,
|
||||
newPassword
|
||||
}: {
|
||||
username: string
|
||||
password: string
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
}): Promise<'SUCCESS' | 'FAIL'> {
|
||||
try {
|
||||
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, [
|
||||
new Change({
|
||||
operation: 'delete',
|
||||
modification: new Attribute({
|
||||
type: 'unicodePwd',
|
||||
values: [encodePassword(password)]
|
||||
})
|
||||
}),
|
||||
new Change({
|
||||
operation: 'add',
|
||||
operation: 'replace',
|
||||
modification: new Attribute({
|
||||
type: 'unicodePwd',
|
||||
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'
|
||||
} catch (err: any) {
|
||||
console.log(err)
|
||||
|
||||
if (err instanceof InvalidCredentialsError) {
|
||||
throw new Error('Usuário ou senha atual incorreta.')
|
||||
}
|
||||
|
@ -80,10 +91,11 @@ export async function updatePassword({
|
|||
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.'
|
||||
)
|
||||
} else throw err
|
||||
}
|
||||
|
||||
console.log('Unexpected error:', err)
|
||||
throw err
|
||||
} finally {
|
||||
await ldapClient.unbind()
|
||||
console.log('unbinded')
|
||||
}
|
||||
return 'FAIL'
|
||||
}
|
||||
|
|
|
@ -16,19 +16,20 @@ export const appRouter = router({
|
|||
updatePassword: input(
|
||||
z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
currentPassword: z.string(),
|
||||
newPassword: z.string().min(8)
|
||||
})
|
||||
).mutation(async ({ input }) => {
|
||||
console.log('input', input)
|
||||
const { username, currentPassword, newPassword } = input
|
||||
|
||||
const { username, password, newPassword } = input
|
||||
try {
|
||||
await updatePassword({
|
||||
username,
|
||||
password,
|
||||
currentPassword,
|
||||
newPassword
|
||||
})
|
||||
|
||||
return 'SUCCESS'
|
||||
} catch (err: any) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<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-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import packageInfo from '../../package.json'
|
||||
|
||||
import { trpc } from "./trpc";
|
||||
|
||||
const data = ref<string>("");
|
||||
|
||||
onMounted(async () => {
|
||||
data.value = await trpc.hello.query();
|
||||
});
|
||||
const version = packageInfo.version
|
||||
</script>
|
||||
|
|
BIN
src/web/assets/favicon.png
Normal file
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 |
|
@ -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 |
|
@ -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>
|
3
src/web/components/Logo.vue
Normal file
3
src/web/components/Logo.vue
Normal file
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<v-img src="@/assets/logo.png" contain />
|
||||
</template>
|
131
src/web/components/MainForm.vue
Normal file
131
src/web/components/MainForm.vue
Normal 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>
|
52
src/web/components/PasswordChecker.vue
Normal file
52
src/web/components/PasswordChecker.vue
Normal 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>
|
|
@ -1,7 +1,42 @@
|
|||
<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>
|
||||
|
||||
<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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user