feat: enhanced dataset management and UI improvements
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m10s
Some checks failed
CI Pipeline / japa-tests (push) Failing after 1m10s
- Submitter/DatasetController.ts: improved validations for time_absolute, time_min, and time_max. - validators/dataset.ts: enhanced validations for time_absolute, time_min, and time_max. - Added new favicon.ico for better branding. - Improved password-meter.vue component with clearer hint messages. - Updated checkStrength.ts: enhanced checkStrength() method for password strength validation. - submitter/Dataset/Create.vue: added form controls for time_min, time_max, and/or time_absolute fields. - submitter/Dataset/Edit.vue: introduced a loading spinner during file upload for better UX.
This commit is contained in:
parent
f67b736a88
commit
d1480b1240
17 changed files with 2682 additions and 1446 deletions
|
@ -77,7 +77,7 @@ const inputElClass = computed(() => {
|
|||
props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
||||
];
|
||||
if (props.icon) {
|
||||
base.push('pl-10');
|
||||
base.push('pl-10', 'pr-10');
|
||||
}
|
||||
return base;
|
||||
});
|
||||
|
@ -136,6 +136,7 @@ if (props.ctrlKFocus) {
|
|||
:class="inputElClass" :readonly="isReadOnly" />
|
||||
<FormControlIcon v-if="icon" :icon="icon" :h="controlIconH" />
|
||||
<slot />
|
||||
<slot name="right" /> <!-- Add slot for right-side content -->
|
||||
<span v-if="showCharCount" class="message-counter" :class="{ 'text-red-500': maxInputLength && maxInputLength < computedValue.length }">
|
||||
{{ computedValue.length }}
|
||||
<template v-if="maxInputLength">
|
||||
|
|
|
@ -269,9 +269,8 @@ const ENDPOINT = 'https://resource.geolba.ac.at/PoolParty/sparql/keyword';
|
|||
// });
|
||||
|
||||
async function handleInput(e: Event) {
|
||||
const target = <HTMLInputElement>e.target;
|
||||
|
||||
console.log(target.value);
|
||||
// const target = <HTMLInputElement>e.target;
|
||||
// console.log(target.value);
|
||||
if (computedValue.value.length >= 2) {
|
||||
data.isOpen = true;
|
||||
return await request(ENDPOINT, computedValue.value);
|
||||
|
|
|
@ -1,53 +1,54 @@
|
|||
import commonPasswords from '../data/commonPasswords';
|
||||
import Trie from './Trie';
|
||||
|
||||
|
||||
|
||||
const checkStrength = (pass: string) => {
|
||||
const score = scorePassword(pass);
|
||||
const scoreLabel = mapScoreToLabel(score);
|
||||
const scoreLabel = mapScoreToLabel(score);
|
||||
const hints = getImprovementHints(pass);
|
||||
return {
|
||||
score,
|
||||
scoreLabel
|
||||
}
|
||||
scoreLabel,
|
||||
hints,
|
||||
};
|
||||
};
|
||||
|
||||
export default checkStrength;
|
||||
|
||||
// Function to score the password based on different criteria
|
||||
const scorePassword = (password: string): number => {
|
||||
if (password.length <= 6) return 0;
|
||||
if (isCommonPassword(password)) return 0;
|
||||
if (password.length <= 8 || isCommonPassword(password)) return 0;
|
||||
|
||||
let score = 0;
|
||||
score += getLengthScore(password);
|
||||
score += getSpecialCharScore(password);
|
||||
score += getCaseMixScore(password);
|
||||
score += getNumberMixScore(password);
|
||||
score += getLengthScore(password); //3
|
||||
score += getSpecialCharScore(password); //1
|
||||
score += getCaseMixScore(password); //1
|
||||
score += getNumberMixScore(password); //1
|
||||
|
||||
return Math.min(score, 4); // Maximum score is 4
|
||||
return Math.min(score, 6); // Maximum score is 6
|
||||
};
|
||||
|
||||
// Initialize the Trie with common passwords
|
||||
const trie = new Trie();
|
||||
commonPasswords.forEach(password => trie.insert(password));
|
||||
commonPasswords.forEach((password) => trie.insert(password));
|
||||
const isCommonPassword = (password: string): boolean => {
|
||||
// return commonPasswords.includes(password);
|
||||
return trie.search(password);
|
||||
};
|
||||
|
||||
// Function to get the score based on password length
|
||||
// Length-based scoring
|
||||
const getLengthScore = (password: string): number => {
|
||||
if (password.length > 20 && !hasRepeatChars(password)) return 3;
|
||||
if (password.length > 12 && !hasRepeatChars(password)) return 2;
|
||||
if (password.length > 8) return 1;
|
||||
return 0;
|
||||
};
|
||||
// Function to check if the password contains repeated characters
|
||||
const hasRepeatChars = (password: string): boolean => {
|
||||
const repeatCharRegex = /(\w)(\1+\1+\1+\1+)/g;
|
||||
return repeatCharRegex.test(password);
|
||||
};
|
||||
|
||||
// const hasRepeatChars = (password: string): boolean => {
|
||||
// const repeatCharRegex = /(\w)(\1+\1+\1+\1+)/g;
|
||||
// return repeatCharRegex.test(password);
|
||||
// };
|
||||
// Check for repeated characters
|
||||
const hasRepeatChars = (password: string): boolean => /(\w)\1{3,}/.test(password);
|
||||
|
||||
// Function to get the score based on the presence of special characters
|
||||
const getSpecialCharScore = (password: string): number => {
|
||||
|
@ -71,23 +72,45 @@ const getNumberMixScore = (password: string): number => {
|
|||
|
||||
// Function to map the score to a corresponding label
|
||||
const mapScoreToLabel = (score: number): string => {
|
||||
const labels = ['risky', 'guessable', 'weak', 'safe', 'secure'];
|
||||
const labels = ['risky', 'guessable', 'weak', 'safe', 'secure', 'safe-secure', 'optimal'];
|
||||
return labels[score] || '';
|
||||
};
|
||||
|
||||
// const nameScore = (score: number): string => {
|
||||
// switch (score) {
|
||||
// case 0:
|
||||
// return 'risky';
|
||||
// case 1:
|
||||
// return 'guessable';
|
||||
// case 2:
|
||||
// return 'weak';
|
||||
// case 3:
|
||||
// return 'safe';
|
||||
// case 4:
|
||||
// return 'secure';
|
||||
// default:
|
||||
// return '';
|
||||
// }
|
||||
// };
|
||||
// Function to get improvement hints
|
||||
const getImprovementHints = (password: string): string[] => {
|
||||
const hints = [];
|
||||
|
||||
if (password.length <= 8) {
|
||||
hints.push('Increase your password length to more than 8 characters for 1 scoring point.');
|
||||
} else {
|
||||
if (password.length > 8 && password.length <= 12) {
|
||||
hints.push('Increase your password length to more than 12 characters for additional scoring point.');
|
||||
} else if (password.length > 12 && password.length <= 20) {
|
||||
hints.push('Increase your password length to more than 20 characters for additional scoring point.');
|
||||
}
|
||||
|
||||
// Check for special character score
|
||||
if (!getSpecialCharScore(password)) {
|
||||
hints.push('Include at least one special character for 1 point.');
|
||||
}
|
||||
|
||||
// Check for case mix score
|
||||
if (!getCaseMixScore(password)) {
|
||||
hints.push('Mix uppercase and lowercase letters for 1 point.');
|
||||
}
|
||||
|
||||
// Check for number mix score
|
||||
if (!getNumberMixScore(password)) {
|
||||
hints.push('Add numbers to your password for 1 point.');
|
||||
}
|
||||
|
||||
if (hasRepeatChars(password) && password.length < 20) {
|
||||
hints.push('Remove repeated characters for 1 point.');
|
||||
}
|
||||
if (hasRepeatChars(password) && password.length > 20) {
|
||||
hints.push('Remove repeated characters for 2 points.');
|
||||
}
|
||||
}
|
||||
|
||||
return hints;
|
||||
};
|
||||
|
|
|
@ -1,63 +1,101 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { checkStrength } from './logic/index';
|
||||
import { mdiFormTextboxPassword } from '@mdi/js';
|
||||
import FormField from '@/Components/FormField.vue';
|
||||
import FormControl from '@/Components/FormControl.vue';
|
||||
|
||||
// Define props
|
||||
const props = defineProps<{
|
||||
password: string;
|
||||
password: string;
|
||||
errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>;
|
||||
}>();
|
||||
|
||||
// Define emits
|
||||
// const emit = defineEmits<{
|
||||
// (event: 'score', payload: { score: number; strength: string }): void;
|
||||
// }>();
|
||||
const emit = defineEmits(['update:password', 'score']);
|
||||
|
||||
const emit = defineEmits(['score']);
|
||||
|
||||
// const score = (event) => {
|
||||
// emit('score', event, payload: { score; strength: string });
|
||||
// };
|
||||
|
||||
// Computed property for password class
|
||||
const passwordClass = computed(() => {
|
||||
if (!props.password) {
|
||||
return null;
|
||||
}
|
||||
// const scoreLabel = checkStrength(props.password);
|
||||
// const score = scorePassword(props.password);
|
||||
const { score, scoreLabel } = checkStrength(props.password);
|
||||
emit('score', score);
|
||||
return {
|
||||
[scoreLabel]: true,
|
||||
// scored: true,
|
||||
};
|
||||
// A local reactive variable for password input
|
||||
const localPassword = ref(props.password);
|
||||
// Watch localPassword and emit changes back to the parent
|
||||
watch(localPassword, (newValue) => {
|
||||
emit('update:password', newValue);
|
||||
});
|
||||
|
||||
type PasswordMetrics = {
|
||||
score: number;
|
||||
scoreLabel: string | null;
|
||||
hints: string[];
|
||||
isSecure: boolean;
|
||||
};
|
||||
|
||||
// Combined computed property for password strength metrics
|
||||
const passwordMetrics = computed<PasswordMetrics>(() => {
|
||||
if (!localPassword.value) {
|
||||
return {
|
||||
score: 0,
|
||||
scoreLabel: null,
|
||||
hints: [],
|
||||
isSecure: false
|
||||
};
|
||||
}
|
||||
const { score, scoreLabel, hints } = checkStrength(localPassword.value);
|
||||
|
||||
emit('score', score);
|
||||
|
||||
return {
|
||||
score,
|
||||
scoreLabel,
|
||||
hints,
|
||||
isSecure: score >= 4
|
||||
};
|
||||
});
|
||||
|
||||
// export default {
|
||||
// name: 'PasswordMeter',
|
||||
// props: {
|
||||
// password: String,
|
||||
// },
|
||||
// emits: ['score'],
|
||||
// computed: {
|
||||
// passwordClass(): object | null {
|
||||
// if (!this.password) {
|
||||
// return null;
|
||||
// }
|
||||
// const strength = checkStrength(this.password);
|
||||
// const score = scorePassword(this.password);
|
||||
// this.$emit('score', { score, strength });
|
||||
// return {
|
||||
// [strength]: true,
|
||||
// scored: true,
|
||||
// };
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="po-password-strength-bar" :class="passwordClass" />
|
||||
<!-- Password input Form -->
|
||||
<FormField label="New password" help="Required. New password" :class="{ 'text-red-400': errors.new_password }">
|
||||
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" required
|
||||
:error="errors.new_password">
|
||||
<!-- Secure Icon -->
|
||||
<template #right>
|
||||
<span v-if="passwordMetrics.isSecure"
|
||||
class="inline-flex justify-center items-center w-10 h-full absolute inset-y-0 right-2 pointer-events-none">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-600" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-10.707a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
<div class="text-red-400 text-sm" v-if="errors.new_password">
|
||||
{{ errors.new_password }}
|
||||
</div>
|
||||
</FormControl>
|
||||
</FormField>
|
||||
|
||||
<!-- Password Strength Bar -->
|
||||
<div class="po-password-strength-bar w-full h-2 rounded transition-all duration-200 mb-4"
|
||||
:class="passwordMetrics.scoreLabel" :style="{ width: `${(passwordMetrics.score / 6) * 100}%` }"
|
||||
role="progressbar" :aria-valuenow="passwordMetrics.score" aria-valuemin="0" aria-valuemax="6"
|
||||
:aria-label="`Password strength: ${passwordMetrics.scoreLabel || 'unknown'}`">
|
||||
</div>
|
||||
|
||||
<!-- Hint Message -->
|
||||
<div v-if="passwordMetrics.hints.length > 0"
|
||||
class="hint-message bg-gray-50 border-l-4 border-gray-300 text-gray-600 p-3 mb-4 rounded-md shadow-sm">
|
||||
<p class="font-medium text-sm mb-2">To improve your password strength:</p>
|
||||
<ul class="list-disc list-inside text-xs space-y-1">
|
||||
<li v-for="(hint, index) in passwordMetrics.hints" :key="index"
|
||||
class="hover:text-gray-800 transition-colors">
|
||||
{{ hint }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Score Display -->
|
||||
<div class="text-gray-700 text-sm">
|
||||
{{ passwordMetrics.score }} / 6 points max
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
@ -88,7 +126,9 @@ const { score, scoreLabel } = checkStrength(props.password);
|
|||
width: 77.5%;
|
||||
}
|
||||
|
||||
.po-password-strength-bar.secure {
|
||||
.po-password-strength-bar.secure,
|
||||
.po-password-strength-bar.safe-secure,
|
||||
.po-password-strength-bar.optimal {
|
||||
background-color: #35cc62;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue