Compare commits

...

12 Commits

Author SHA1 Message Date
32fb68c5ea tap to reveal and wait functionality
some layout improvements
2024-06-16 23:23:21 +05:30
ba400fd5f6 Automatic sort handling, layout improvements 2024-06-16 23:23:01 +05:30
72583eb2ec Changed buttons to icons
Savign secret now refreshes the component
2024-06-16 23:19:25 +05:30
5dd375b796 prevent horizontal scroll bar 2024-06-16 23:18:33 +05:30
649cadd591 new icons 2024-06-16 23:18:18 +05:30
64b5708046 show secret checkbox and using local storage (WIP) 2024-06-16 15:02:07 +05:30
8b9b903ad5 style changes and no longer calls APIs to update OTP 2024-06-16 15:01:07 +05:30
4727837919 full page on desktop 2024-06-16 14:59:59 +05:30
52a3f852fe OTP updates without API call 2024-06-16 14:59:48 +05:30
08a4882eaa Added sorting options and show secret toggle 2024-06-16 12:25:00 +05:30
ee31d128d5 timer bar doesn't show up on login page 2024-06-16 12:24:20 +05:30
3f1ddacce3 handled bug cannot create secret in empty file 2024-06-16 12:23:41 +05:30
9 changed files with 198 additions and 72 deletions

View File

@ -6,27 +6,28 @@ import ListSecrets from "./components/ListSecrets.vue";
<template> <template>
<div> <div>
<div id="header"> <div id="header" class="header">
<span class="logo">FastAuth</span> <span class="logo">FastAuth</span>
<div class="header-buttons" v-if="loggedin"> <div class="header-buttons" v-if="loggedin">
<el-button type="primary" class="ml-2" @click="creationDialog = true"> <button class="header-button" title="Refresh secrets" @click="refresh">
Create <img src="./assets/refresh-icon.png" class="button-icon" />
</el-button> </button>
<el-button type="warning" class="ml-2" @click="logout">Logout</el-button> <button class="header-button" title="logout" @click="logout">
<el-button type="error" @click="refresh">Refresh</el-button> <img src="./assets/logout-icon.png" class="button-icon" />
</button>
</div> </div>
</div> </div>
<div class="timer" :style="{ width: timerWidth + '%' }"></div>
<el-dialog v-model="creationDialog" title="Add a new TOTP secret" width="80vw"> <el-dialog v-model="creationDialog" title="Add a new TOTP secret" width="80vw">
<CreateSecret @close="creationDialog = false" /> <CreateSecret @close="secretSaved" />
</el-dialog> </el-dialog>
<el-dialog v-model="editDialog" title="Edit TOTP secret" width="80vw"> <el-dialog v-model="editDialog" title="Edit TOTP secret" width="80vw">
<CreateSecret :editSecret="editingSecret" @close="editDialog = false" /> <CreateSecret :editSecret="editingSecret" @close="secretSaved" />
</el-dialog> </el-dialog>
<div class="container"> <div class="container">
<div class="timer" v-if="loggedin" :style="{ width: timerWidth + '%' }"></div>
<HomePage <HomePage
msg="You did it!" msg="You did it!"
@loggedin=" @loggedin="
@ -41,6 +42,7 @@ import ListSecrets from "./components/ListSecrets.vue";
</el-button> --> </el-button> -->
<ListSecrets :key="listUpdated" v-if="showSecrets && loggedin" @edit="editSecret" /> <ListSecrets :key="listUpdated" v-if="showSecrets && loggedin" @edit="editSecret" />
</div> </div>
<button class="create-floating" @click="creationDialog = true">+</button>
</div> </div>
</template> </template>
@ -55,7 +57,7 @@ export default {
apiBaseUrl: "http://localhost:8000", apiBaseUrl: "http://localhost:8000",
editDialog: false, editDialog: false,
editingSecret: {}, editingSecret: {},
timerWidth: 100, timerWidth: 0,
}; };
}, },
methods: { methods: {
@ -66,9 +68,8 @@ export default {
secretSaved() { secretSaved() {
this.creationDialog = false; this.creationDialog = false;
console.log("before update", this.listUpdated); this.editDialog = false;
this.listUpdated += 1; this.listUpdated += 1;
console.log("after update", this.listUpdated);
}, },
async validateToken() { async validateToken() {
@ -112,15 +113,12 @@ export default {
}, },
startTimer() { startTimer() {
// console.log("start timer called");
this.interval = setInterval(() => { this.interval = setInterval(() => {
const now = new Date(); const now = new Date();
const seconds = now.getSeconds(); const seconds = now.getSeconds();
const remainingTime = (seconds > 30 ? 60 : 30) - seconds; const remainingTime = (seconds > 30 ? 60 : 30) - seconds;
// console.log(remainingTime);
this.timerWidth = (remainingTime / 30) * 100; this.timerWidth = (remainingTime / 30) * 100;
if (remainingTime === 30) {
this.refresh();
}
}, 1000); }, 1000);
}, },
}, },
@ -139,19 +137,9 @@ export default {
}; };
</script> </script>
<style> <style scoped>
.header {
position: absolute;
width: 100vw;
height: 3rem;
padding: 0.3rem;
left: 0;
top: 0;
background-color: aquamarine;
}
.container { .container {
margin-top: 0; margin-top: 6vh;
} }
.logoutBtn { .logoutBtn {
@ -163,10 +151,10 @@ export default {
display: none !important; display: none !important;
} }
#header { .header {
width: 100vw; width: 100vw;
height: 3rem; height: 48px;
position: absolute; position: fixed;
left: 0; left: 0;
top: 0; top: 0;
background-color: rgb(170, 247, 247); background-color: rgb(170, 247, 247);
@ -175,15 +163,45 @@ export default {
padding: 0 12px 0 12px; padding: 0 12px 0 12px;
justify-content: space-between; justify-content: space-between;
box-shadow: 2px 0px 8px #aaa; box-shadow: 2px 0px 8px #aaa;
z-index: 999;
} }
.logo { .logo {
font-size: 1.3rem; font-size: 1.3rem;
font-weight: 700; font-weight: 700;
margin-left: 2vw;
} }
.timer { .timer {
margin-top: 1.2rem; position: fixed;
bottom: 4px;
height: 0.3rem; height: 0.3rem;
background-color: green; background-color: green;
} }
.create-floating {
position: fixed;
bottom: 4vh;
right: 4vw;
height: 40px;
width: 40px;
border: none;
border-radius: 20px;
box-shadow: 4px 4px 6px #999;
background-color: teal;
color: white;
font-size: 1.6rem;
cursor: pointer;
}
.button-icon {
width: 24px;
margin-top: 8px;
height: 24px;
opacity: 0.6;
}
.header-button {
border: none;
background: none;
}
</style> </style>

View File

@ -65,7 +65,7 @@ body {
transition: transition:
color 0.5s, color 0.5s,
background-color 0.5s; */ background-color 0.5s; */
line-height: 1.6; line-height: 1.2;
font-family: font-family:
/* Inter, /* Inter,
-apple-system, -apple-system,

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,10 +1,11 @@
@import './base.css'; @import './base.css';
#app { #app {
max-width: 1280px; /* max-width: 1280px; */
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 1rem;
font-weight: normal; font-weight: normal;
overflow-x: hidden;
} }
a, a,
@ -24,12 +25,13 @@ a,
@media (min-width: 1024px) { @media (min-width: 1024px) {
body { body {
display: flex; display: flex;
place-items: center; /* place-items: center; */
margin-top: 2rem;
} }
#app { #app {
display: grid; /* display: grid; */
grid-template-columns: 1fr 1fr; /* grid-template-columns: 1fr 1fr; */
padding: 0 2rem; padding: 0 2rem;
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<div> <div>
<br />
<div class="filter-buttons"> <div class="filter-buttons">
<el-button-group> <el-button-group>
<el-button <el-button
@ -16,6 +15,30 @@
>Clear</el-button >Clear</el-button
> >
</el-button-group> </el-button-group>
<div>
<el-form-item label="Show OTPs">
<el-switch v-model="showSecrets" active-value="yes" inactive-value="no" />
</el-form-item>
</div>
<div class="sort-options">
<span>
<el-form-item label="Sort">
<el-select
v-model="currentSort"
placeholder="Select"
style="width: 80px"
@change="sortItems"
>
<el-option
v-for="item in sortOptions"
:key="item.key"
:label="item.name"
:value="item.key"
/>
</el-select>
</el-form-item>
</span>
</div>
</div> </div>
</div> </div>
@ -26,6 +49,8 @@
:username="secret.username" :username="secret.username"
:secret="secret.secret" :secret="secret.secret"
:id="secret.id" :id="secret.id"
:showOtp="showSecrets"
:key="showSecrets"
@edit="editSecret" @edit="editSecret"
/> />
</span> </span>
@ -57,6 +82,18 @@ export default {
currentFilter: [], currentFilter: [],
loadCards: false, loadCards: false,
showClear: false, showClear: false,
showSecrets: "yes",
currentSort: "asc",
sortOptions: [
{
name: "A-Z",
key: "asc",
},
{
name: "Z-A",
key: "desc",
},
],
}; };
}, },
@ -87,6 +124,7 @@ export default {
this.secretsList.push(row); this.secretsList.push(row);
}); });
this.filteredSecretsList = this.secretsList; this.filteredSecretsList = this.secretsList;
this.sortItems();
this.loadCards = true; this.loadCards = true;
}, },
@ -129,6 +167,14 @@ export default {
this.filteredSecretsList = this.secretsList; this.filteredSecretsList = this.secretsList;
this.showClear = false; this.showClear = false;
}, },
sortItems() {
if (this.currentSort === "asc") {
this.filteredSecretsList.sort((a, b) => a.issuer.localeCompare(b.issuer));
} else if (this.currentSort === "desc") {
this.filteredSecretsList.sort((a, b) => b.issuer.localeCompare(a.issuer));
}
},
}, },
computed: {}, computed: {},
@ -137,11 +183,28 @@ export default {
currentFilter: function () { currentFilter: function () {
const filteredList = this.secretsList.filter(this.checkFirstLetter); const filteredList = this.secretsList.filter(this.checkFirstLetter);
this.filteredSecretsList = filteredList; this.filteredSecretsList = filteredList;
this.sortItems();
},
currentSort: function () {
localStorage.setItem("currentSort", this.currentSort);
},
showSecrets: function () {
localStorage.setItem("showSecrets", this.showSecrets);
},
secretsList: function () {
this.currentSort = localStorage.getItem("currentSort");
this.filteredSecretsList = this.secretsList;
this.sortItems();
}, },
}, },
mounted() { mounted() {
this.listSecrets(); this.listSecrets();
this.showSecrets = localStorage.getItem("showSecrets");
this.currentSort = localStorage.getItem("currentSort");
}, },
}; };
</script> </script>
@ -151,13 +214,21 @@ export default {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
max-width: 90vw; justify-content: space-evenly;
max-width: 96vw;
} }
.filter-buttons { .filter-buttons {
width: 90%; width: 96vw;
display: flex; display: flex;
margin-bottom: 20px; margin: 40px 0 20px 0;
justify-content: center; justify-content: space-around;
flex-wrap: wrap;
}
.sort-options {
display: flex;
justify-content: space-between;
min-width: 80px;
} }
</style> </style>

View File

@ -1,25 +1,28 @@
<template> <template>
<div class="wrapper"> <div
class="wrapper"
@mouseenter="[(otpShown = true), (iconOpacity = 1)]"
@mouseleave="
[(otpShown = showOtp === 'yes' ? true : tapped ? true : false), (iconOpacity = 0.1)]
"
>
<div class="card-top"> <div class="card-top">
<span class="issuer">{{ issuer }}</span> <span class="issuer">{{ issuer }}</span>
<button class="edit-button" @click="editSecret"> <button class="card-button" @click="editSecret">
<img src="../assets/edit-icon.png" class="button-icon" /></button <img src="../assets/edit-icon.png" class="button-icon" /></button
><br /> ><br />
</div> </div>
<span class="user">{{ username }}</span> <br /> <span class="user">{{ username }}</span> <br />
<span <div class="otp-container">
class="otp" <span class="otp" @click="[tapReveal($event), copyOtp($event)]">
@click="copyOtp" <span v-if="otpShown">{{ this.otp }}</span>
@mouseenter="otpHidden = false" <span v-else> &#8226;&#8226;&#8226;&#8226;&#8226;&#8226; </span>
@mouseleave="otpHidden = true" </span>
> <button class="card-button" @click="copyOtp">
<span v-if="otpHidden">******</span> <img src="../assets/copy-icon.png" class="button-icon" />
<span v-else>{{ generateTotp(secret) }}</span> </button>
</span> </div>
<button class="edit-button" @click="copyOtp">
<img src="../assets/copy-icon.png" class="button-icon" />
</button>
</div> </div>
</template> </template>
@ -31,24 +34,29 @@ export default {
issuer: String, issuer: String,
username: String, username: String,
secret: String, secret: String,
showOtp: {
type: String,
default: "no",
},
}, },
data() { data() {
return { return {
otp: 123456, otp: 123456,
notes: "", notes: "",
otpHidden: true, otpShown: false,
iconOpacity: 0.1,
tapped: false,
revealTime: 10,
}; };
}, },
methods: { methods: {
generateTotp(secret) { generateTotp() {
const { otp, expires } = TOTP.generate(secret); const { otp, expires } = TOTP.generate(this.secret);
const remaining = (expires - Date.now()) / 30000; const now = new Date();
this.remainingTime = remaining / 1000; const remainingTime = expires - now;
this.timerWidth = remaining * 100;
// console.log("hello");
this.otp = otp; this.otp = otp;
return otp; setTimeout(this.generateTotp, remainingTime);
}, },
async copyOtp() { async copyOtp() {
@ -62,18 +70,33 @@ export default {
}); });
}, },
async tapReveal() {
console.log("tapped");
this.otpShown = true;
this.tapped = true;
setTimeout(() => {
this.otpShown = this.showOtp === "yes" ? true : false;
this.tapped = false;
}, this.revealTime * 1000);
},
editSecret() { editSecret() {
this.$emit("edit", this.id); this.$emit("edit", this.id);
}, },
}, },
mounted() {}, mounted() {
this.otpShown = this.showOtp === "yes" ? true : false;
this.generateTotp();
},
created() {},
}; };
</script> </script>
<style> <style>
.wrapper { .wrapper {
height: 110px; height: 90px;
width: 160px; width: 160px;
/* border: 1px solid black; */ /* border: 1px solid black; */
border-radius: 4px; border-radius: 4px;
@ -92,22 +115,30 @@ export default {
} }
.otp { .otp {
font-size: 2rem; margin-top: 0.4rem;
font-size: 1.8rem;
} }
.card-top { .card-top {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.edit-button { .card-button {
position: relative; position: relative;
right: 0; right: 0;
margin-left: auto; margin-left: auto;
border: none; border: none;
background: none;
} }
.button-icon { .button-icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
opacity: v-bind("iconOpacity");
}
.otp-container {
display: flex;
justify-content: space-between;
} }
</style> </style>

View File

@ -131,7 +131,11 @@ async def create_secret(secret: Secret, current_user: dict = Depends(get_current
text = f.read() text = f.read()
if text: if text:
data.extend(json.loads(text)) data.extend(json.loads(text))
secret_id = max(i['id'] for i in data) + 1
if data:
secret_id = max(i['id'] for i in data) + 1
else:
secret_id = 0
secret.id = secret_id secret.id = secret_id
secret.user_id = current_user['id'] secret.user_id = current_user['id']
encryption_key = current_user['encryption_key'].encode() encryption_key = current_user['encryption_key'].encode()

View File

@ -36,6 +36,6 @@ typer==0.12.3
typing_extensions==4.12.2 typing_extensions==4.12.2
ujson==5.10.0 ujson==5.10.0
uvicorn==0.30.1 uvicorn==0.30.1
uvloop==0.19.0 # uvloop==0.19.0
watchfiles==0.22.0 watchfiles==0.22.0
websockets==12.0 websockets==12.0