Compare commits
27 Commits
50094eb3c8
...
master
Author | SHA1 | Date | |
---|---|---|---|
32fb68c5ea | |||
ba400fd5f6 | |||
72583eb2ec | |||
5dd375b796 | |||
649cadd591 | |||
64b5708046 | |||
8b9b903ad5 | |||
4727837919 | |||
52a3f852fe | |||
08a4882eaa | |||
ee31d128d5 | |||
3f1ddacce3 | |||
334bb97d35 | |||
726e8a208a | |||
8210f4957e | |||
658c0b091c | |||
879bf67b22 | |||
e9c2ccc115 | |||
0514a7ce3d | |||
4bf48b9538 | |||
2121f86a33 | |||
1377d63347 | |||
956faf2e3c | |||
e359526b17 | |||
2593264d24 | |||
a85e9734af | |||
de9c5b5c87 |
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"element-plus": "^2.7.5",
|
"element-plus": "^2.7.5",
|
||||||
|
"totp-generator": "^1.0.0",
|
||||||
"vue": "^3.4.21"
|
"vue": "^3.4.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -1830,6 +1831,14 @@
|
|||||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/jssha": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@ -2361,6 +2370,14 @@
|
|||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/totp-generator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/totp-generator/-/totp-generator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Iu/1Lk60/MH8FE+5cDWPiGbwKK1hxzSq+KT9oSqhZ1BEczGIKGcN50bP0WMLiIZKRg7t29iWLxw6f81TICQdoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"jssha": "^3.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"element-plus": "^2.7.5",
|
"element-plus": "^2.7.5",
|
||||||
|
"totp-generator": "^1.0.0",
|
||||||
"vue": "^3.4.21"
|
"vue": "^3.4.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,12 +1,48 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import CreateSecret from "./components/CreateSecret.vue";
|
import CreateSecret from "./components/CreateSecret.vue";
|
||||||
import HelloWorld from "./components/HelloWorld.vue";
|
import HomePage from "./components/HomePage.vue";
|
||||||
|
import ListSecrets from "./components/ListSecrets.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<HelloWorld msg="You did it!" @loggedin="loggedin = true" />
|
<div id="header" class="header">
|
||||||
<CreateSecret v-if="loggedin" />
|
<span class="logo">FastAuth</span>
|
||||||
|
<div class="header-buttons" v-if="loggedin">
|
||||||
|
<button class="header-button" title="Refresh secrets" @click="refresh">
|
||||||
|
<img src="./assets/refresh-icon.png" class="button-icon" />
|
||||||
|
</button>
|
||||||
|
<button class="header-button" title="logout" @click="logout">
|
||||||
|
<img src="./assets/logout-icon.png" class="button-icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="creationDialog" title="Add a new TOTP secret" width="80vw">
|
||||||
|
<CreateSecret @close="secretSaved" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="editDialog" title="Edit TOTP secret" width="80vw">
|
||||||
|
<CreateSecret :editSecret="editingSecret" @close="secretSaved" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="timer" v-if="loggedin" :style="{ width: timerWidth + '%' }"></div>
|
||||||
|
<HomePage
|
||||||
|
msg="You did it!"
|
||||||
|
@loggedin="
|
||||||
|
loggedin = true;
|
||||||
|
showSecrets = true;
|
||||||
|
"
|
||||||
|
v-if="!loggedin"
|
||||||
|
/>
|
||||||
|
<!-- <el-button @click="showSecrets = true" v-if="loggedin"> Show secrets </el-button>
|
||||||
|
<el-button @click="showSecrets = false" v-if="showSecrets && loggedin">
|
||||||
|
Hide secrets
|
||||||
|
</el-button> -->
|
||||||
|
<ListSecrets :key="listUpdated" v-if="showSecrets && loggedin" @edit="editSecret" />
|
||||||
|
</div>
|
||||||
|
<button class="create-floating" @click="creationDialog = true">+</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -15,8 +51,157 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loggedin: false,
|
loggedin: false,
|
||||||
|
showSecrets: false,
|
||||||
|
creationDialog: false,
|
||||||
|
listUpdated: 1,
|
||||||
|
apiBaseUrl: "http://localhost:8000",
|
||||||
|
editDialog: false,
|
||||||
|
editingSecret: {},
|
||||||
|
timerWidth: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
logout() {
|
||||||
|
sessionStorage.removeItem("token");
|
||||||
|
this.loggedin = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
secretSaved() {
|
||||||
|
this.creationDialog = false;
|
||||||
|
this.editDialog = false;
|
||||||
|
this.listUpdated += 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
async validateToken() {
|
||||||
|
const url = `${this.apiBaseUrl}/validate-token`;
|
||||||
|
const token = sessionStorage.getItem("token");
|
||||||
|
const requestOptions = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, requestOptions)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ("message" in response) {
|
||||||
|
if (response["message"] === "authenticated") {
|
||||||
|
console.log("token validated");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
editSecret(secret) {
|
||||||
|
this.editingSecret = secret;
|
||||||
|
// console.log(this.editingSecret);
|
||||||
|
this.editDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.listUpdated += 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
startTimer() {
|
||||||
|
// console.log("start timer called");
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const seconds = now.getSeconds();
|
||||||
|
const remainingTime = (seconds > 30 ? 60 : 30) - seconds;
|
||||||
|
this.timerWidth = (remainingTime / 30) * 100;
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
if ("token" in sessionStorage) {
|
||||||
|
const tokenValid = await this.validateToken();
|
||||||
|
|
||||||
|
if (tokenValid) {
|
||||||
|
this.loggedin = true;
|
||||||
|
this.showSecrets = true;
|
||||||
|
}
|
||||||
|
this.startTimer();
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 6vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoutBtn {
|
||||||
|
/* position: relative; */
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.el-page-header__back {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100vw;
|
||||||
|
height: 48px;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background-color: rgb(170, 247, 247);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px 0 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 2px 0px 8px #aaa;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 4px;
|
||||||
|
height: 0.3rem;
|
||||||
|
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>
|
||||||
|
@ -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,
|
||||||
|
BIN
frontend/src/assets/copy-icon.png
Normal file
BIN
frontend/src/assets/copy-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
frontend/src/assets/edit-icon.png
Normal file
BIN
frontend/src/assets/edit-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
frontend/src/assets/logout-icon.png
Normal file
BIN
frontend/src/assets/logout-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
BIN
frontend/src/assets/refresh-icon.png
Normal file
BIN
frontend/src/assets/refresh-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h3>
|
|
||||||
{{ title }}
|
|
||||||
</h3>
|
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<el-form :model="form" label-width="auto">
|
<el-form :model="form" label-width="auto">
|
||||||
<el-form-item label="Issuer">
|
<el-form-item label="Issuer">
|
||||||
@ -18,43 +15,84 @@
|
|||||||
<el-input v-model="form.notes" />
|
<el-input v-model="form.notes" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-button @click="createSecret" type="primary">Create secret</el-button>
|
<el-button @click="createSecret" type="primary" v-if="showAddMore"
|
||||||
|
>Save & Add More</el-button
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
@click="
|
||||||
|
createSecret();
|
||||||
|
closeDialog();
|
||||||
|
"
|
||||||
|
type="primary"
|
||||||
|
>Save & Close</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
editSecret: Object,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
title: "Create Secret",
|
title: "Create Secret",
|
||||||
apiBaseUrl: "http://localhost:8000",
|
apiBaseUrl: "http://localhost:8000",
|
||||||
|
id: null,
|
||||||
form: {
|
form: {
|
||||||
issuer: "asdfasdf",
|
issuer: "",
|
||||||
username: "asdfasdf",
|
username: "",
|
||||||
secret: "asdfasdf",
|
secret: "",
|
||||||
notes: "asdfasdf",
|
notes: "",
|
||||||
},
|
},
|
||||||
|
method: "POST",
|
||||||
|
showAddMore: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async createSecret() {
|
async createSecret() {
|
||||||
const url = `${this.apiBaseUrl}/secret`;
|
const url = `${this.apiBaseUrl}/secret`;
|
||||||
const token = localStorage.getItem("token");
|
const token = sessionStorage.getItem("token");
|
||||||
|
const bodyData = { data: btoa(JSON.stringify(this.form)) };
|
||||||
|
if (this.method === "PUT") {
|
||||||
|
bodyData["id"] = this.id;
|
||||||
|
}
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: "POST",
|
method: this.method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ data: btoa(JSON.stringify(this.form)) }),
|
body: JSON.stringify(bodyData),
|
||||||
};
|
};
|
||||||
console.log(requestOptions);
|
console.log(requestOptions);
|
||||||
await fetch(url, requestOptions)
|
await fetch(url, requestOptions)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => console.log(data));
|
.then((data) => console.log(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
closeDialog() {
|
||||||
|
(this.form.issuer = ""),
|
||||||
|
(this.form.username = ""),
|
||||||
|
(this.form.notes = ""),
|
||||||
|
(this.form.secret = "");
|
||||||
|
this.$emit("close", true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
// console.log(this.editSecret.id);
|
||||||
|
if (this.editSecret) {
|
||||||
|
this.id = this.editSecret.id;
|
||||||
|
this.form.issuer = this.editSecret.issuer;
|
||||||
|
this.form.username = this.editSecret.username;
|
||||||
|
this.form.secret = this.editSecret.secret;
|
||||||
|
this.form.notes = this.editSecret.notes;
|
||||||
|
this.method = "PUT";
|
||||||
|
this.showAddMore = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,12 +1,3 @@
|
|||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
msg: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-form :model="form" label-width="auto">
|
<el-form :model="form" label-width="auto">
|
||||||
@ -58,7 +49,7 @@ export default {
|
|||||||
if ("message" in response) {
|
if ("message" in response) {
|
||||||
if (response.message === "authenticated") {
|
if (response.message === "authenticated") {
|
||||||
const token = response.accessToken;
|
const token = response.accessToken;
|
||||||
localStorage.setItem("token", token);
|
sessionStorage.setItem("token", token);
|
||||||
this.$emit("loggedin", true);
|
this.$emit("loggedin", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +1,234 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div>
|
||||||
|
<div>
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
v-for="band in filterBands"
|
||||||
|
:key="band"
|
||||||
|
@click="filterTable(band)"
|
||||||
|
>
|
||||||
|
{{ band }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" @click="clearFilter" v-if="showClear"
|
||||||
|
>Clear</el-button
|
||||||
|
>
|
||||||
|
</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 id="cards" v-if="loadCards">
|
||||||
|
<span v-for="secret in filteredSecretsList" :key="secret.id">
|
||||||
|
<SecretCard
|
||||||
|
:issuer="secret.issuer"
|
||||||
|
:username="secret.username"
|
||||||
|
:secret="secret.secret"
|
||||||
|
:id="secret.id"
|
||||||
|
:showOtp="showSecrets"
|
||||||
|
:key="showSecrets"
|
||||||
|
@edit="editSecret"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { TOTP } from "totp-generator";
|
||||||
|
|
||||||
|
import SecretCard from "./SecretCard.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { SecretCard },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
message: "Hello List Secret",
|
message: "Hello List Secret",
|
||||||
|
apiBaseUrl: "http://localhost:8000",
|
||||||
|
secretsList: [],
|
||||||
|
filteredSecretsList: [],
|
||||||
|
filterBands: ["A-C", "D-I", "J-O", "P-S", "T-Z"],
|
||||||
|
filterBandsVals: {
|
||||||
|
"A-C": ["A", "B", "C"],
|
||||||
|
"D-I": ["D", "E", "F", "G", "H", "I"],
|
||||||
|
"J-O": ["J", "K", "L", "M", "N", "O"],
|
||||||
|
"P-S": ["P", "Q", "R", "S"],
|
||||||
|
"T-Z": ["T", "U", "V", "W", "X", "Y", "Z"],
|
||||||
|
},
|
||||||
|
currentFilter: [],
|
||||||
|
loadCards: false,
|
||||||
|
showClear: false,
|
||||||
|
showSecrets: "yes",
|
||||||
|
currentSort: "asc",
|
||||||
|
sortOptions: [
|
||||||
|
{
|
||||||
|
name: "A-Z",
|
||||||
|
key: "asc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Z-A",
|
||||||
|
key: "desc",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async listSecrets() {
|
||||||
|
const url = `${this.apiBaseUrl}/secret`;
|
||||||
|
const token = sessionStorage.getItem("token");
|
||||||
|
const requestOptions = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
console.log(requestOptions);
|
||||||
|
const response = await fetch(url, requestOptions)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
// console.log(data);
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
|
response.forEach((element) => {
|
||||||
|
// console.log(element);
|
||||||
|
const row = this.parseSecret(element.data);
|
||||||
|
row["id"] = element["id"];
|
||||||
|
this.secretsList.push(row);
|
||||||
|
});
|
||||||
|
this.filteredSecretsList = this.secretsList;
|
||||||
|
this.sortItems();
|
||||||
|
this.loadCards = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
parseSecret(gibberish) {
|
||||||
|
const jsonString = atob(gibberish);
|
||||||
|
const secret = JSON.parse(jsonString);
|
||||||
|
return secret;
|
||||||
|
},
|
||||||
|
|
||||||
|
generateTotp(secret) {
|
||||||
|
const { otp, expires } = TOTP.generate(secret);
|
||||||
|
console.log(expires);
|
||||||
|
return otp;
|
||||||
|
},
|
||||||
|
|
||||||
|
editSecret(id) {
|
||||||
|
const editingSecret = this.secretsList.filter((element) => {
|
||||||
|
return element.id === id;
|
||||||
|
});
|
||||||
|
// console.log(editingSecret);
|
||||||
|
this.$emit("edit", editingSecret[0]);
|
||||||
|
},
|
||||||
|
|
||||||
|
filterTable(band) {
|
||||||
|
const letters = this.filterBandsVals[band];
|
||||||
|
this.currentFilter = letters;
|
||||||
|
this.showClear = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
checkFirstLetter(row) {
|
||||||
|
const firstLetter = row.issuer[0].toUpperCase();
|
||||||
|
const index = this.currentFilter.indexOf(firstLetter);
|
||||||
|
if (index >= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFilter() {
|
||||||
|
this.filteredSecretsList = this.secretsList;
|
||||||
|
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: {},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
currentFilter: function () {
|
||||||
|
const filteredList = this.secretsList.filter(this.checkFirstLetter);
|
||||||
|
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() {
|
||||||
|
this.listSecrets();
|
||||||
|
this.showSecrets = localStorage.getItem("showSecrets");
|
||||||
|
this.currentSort = localStorage.getItem("currentSort");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#cards {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
max-width: 96vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
width: 96vw;
|
||||||
|
display: flex;
|
||||||
|
margin: 40px 0 20px 0;
|
||||||
|
justify-content: space-around;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
144
frontend/src/components/SecretCard.vue
Normal file
144
frontend/src/components/SecretCard.vue
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="wrapper"
|
||||||
|
@mouseenter="[(otpShown = true), (iconOpacity = 1)]"
|
||||||
|
@mouseleave="
|
||||||
|
[(otpShown = showOtp === 'yes' ? true : tapped ? true : false), (iconOpacity = 0.1)]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="card-top">
|
||||||
|
<span class="issuer">{{ issuer }}</span>
|
||||||
|
<button class="card-button" @click="editSecret">
|
||||||
|
<img src="../assets/edit-icon.png" class="button-icon" /></button
|
||||||
|
><br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="user">{{ username }}</span> <br />
|
||||||
|
<div class="otp-container">
|
||||||
|
<span class="otp" @click="[tapReveal($event), copyOtp($event)]">
|
||||||
|
<span v-if="otpShown">{{ this.otp }}</span>
|
||||||
|
<span v-else> •••••• </span>
|
||||||
|
</span>
|
||||||
|
<button class="card-button" @click="copyOtp">
|
||||||
|
<img src="../assets/copy-icon.png" class="button-icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { TOTP } from "totp-generator";
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
id: Number,
|
||||||
|
issuer: String,
|
||||||
|
username: String,
|
||||||
|
secret: String,
|
||||||
|
showOtp: {
|
||||||
|
type: String,
|
||||||
|
default: "no",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
otp: 123456,
|
||||||
|
notes: "",
|
||||||
|
otpShown: false,
|
||||||
|
iconOpacity: 0.1,
|
||||||
|
tapped: false,
|
||||||
|
revealTime: 10,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
generateTotp() {
|
||||||
|
const { otp, expires } = TOTP.generate(this.secret);
|
||||||
|
const now = new Date();
|
||||||
|
const remainingTime = expires - now;
|
||||||
|
this.otp = otp;
|
||||||
|
setTimeout(this.generateTotp, remainingTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
async copyOtp() {
|
||||||
|
await navigator.clipboard
|
||||||
|
.writeText(this.otp)
|
||||||
|
.then(() => {
|
||||||
|
console.log("copying successful");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("copy failed", err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
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() {
|
||||||
|
this.$emit("edit", this.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.otpShown = this.showOtp === "yes" ? true : false;
|
||||||
|
this.generateTotp();
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
height: 90px;
|
||||||
|
width: 160px;
|
||||||
|
/* border: 1px solid black; */
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px 4px 12px;
|
||||||
|
box-shadow: 1px 1px 8px #ccc;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issuer {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
color: gray;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.card-button {
|
||||||
|
position: relative;
|
||||||
|
right: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: v-bind("iconOpacity");
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
58
main.py
58
main.py
@ -123,9 +123,7 @@ async def login(user: UserLogin):
|
|||||||
@app.post("/secret")
|
@app.post("/secret")
|
||||||
async def create_secret(secret: Secret, current_user: dict = Depends(get_current_user)):
|
async def create_secret(secret: Secret, current_user: dict = Depends(get_current_user)):
|
||||||
"""
|
"""
|
||||||
Stores and encrypted secret for the user.
|
Stores an encrypted secret for the user.
|
||||||
|
|
||||||
The encrypted secret is unreadable on the server and is encrypted on the front-end
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
@ -133,6 +131,12 @@ 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))
|
||||||
|
|
||||||
|
if data:
|
||||||
|
secret_id = max(i['id'] for i in data) + 1
|
||||||
|
else:
|
||||||
|
secret_id = 0
|
||||||
|
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()
|
||||||
|
|
||||||
@ -146,6 +150,40 @@ async def create_secret(secret: Secret, current_user: dict = Depends(get_current
|
|||||||
return secret
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/secret")
|
||||||
|
async def update_secret(secret: Secret, current_user: dict = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Updates an encrypted secret for the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = []
|
||||||
|
with open('database/secrets.json', 'r') as f:
|
||||||
|
text = f.read()
|
||||||
|
if text:
|
||||||
|
data.extend(json.loads(text))
|
||||||
|
|
||||||
|
if secret.id is None:
|
||||||
|
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Id must be passed for updating secret")
|
||||||
|
|
||||||
|
secret.user_id = current_user['id']
|
||||||
|
|
||||||
|
found_secrets = [(i, j) for i, j in enumerate(data) if j['user_id'] == secret.user_id and j['id']==secret.id]
|
||||||
|
if not found_secrets:
|
||||||
|
raise HTTPException(status.HTTP_400_BAD_REQUEST, deatil="Secret with this Id not found for this user")
|
||||||
|
|
||||||
|
secret_pos = found_secrets[0][0]
|
||||||
|
|
||||||
|
encryption_key = current_user['encryption_key'].encode()
|
||||||
|
encrypted_data = fernet_encrypt(secret.data.encode(), encryption_key)
|
||||||
|
|
||||||
|
secret.data = encrypted_data.decode('utf-8')
|
||||||
|
data[secret_pos] = jsonable_encoder(secret)
|
||||||
|
|
||||||
|
with open('database/secrets.json', 'w') as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
@app.get('/secret')
|
@app.get('/secret')
|
||||||
async def list_secret(current_user: dict = Depends(get_current_user)):
|
async def list_secret(current_user: dict = Depends(get_current_user)):
|
||||||
"""Returns the encrypted secrets of the user."""
|
"""Returns the encrypted secrets of the user."""
|
||||||
@ -159,10 +197,20 @@ async def list_secret(current_user: dict = Depends(get_current_user)):
|
|||||||
user_id = current_user['id']
|
user_id = current_user['id']
|
||||||
encryption_key = current_user['encryption_key'].encode()
|
encryption_key = current_user['encryption_key'].encode()
|
||||||
|
|
||||||
user_secrets = [i for i in data if i['user_id']==user_id]
|
user_secrets = [i for i in data if i['user_id']==user_id and i['active']]
|
||||||
for secret in user_secrets:
|
for secret in user_secrets:
|
||||||
cur_data = secret['data']
|
cur_data = secret['data']
|
||||||
decrypted_data = fernet_decrypt(cur_data, encryption_key)
|
decrypted_data = fernet_decrypt(cur_data, encryption_key)
|
||||||
secret['data'] = decrypted_data
|
secret['data'] = decrypted_data
|
||||||
|
|
||||||
return user_secrets
|
return user_secrets
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/validate-token')
|
||||||
|
async def validate_token(current_user: dict = Depends(get_current_user)):
|
||||||
|
user_id = current_user['id']
|
||||||
|
print("user_id: ", user_id)
|
||||||
|
if user_id is not None:
|
||||||
|
return {'message': 'authenticated'}
|
||||||
|
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
@ -17,10 +17,9 @@ class UserLogin(BaseModel):
|
|||||||
password: str
|
password: str
|
||||||
|
|
||||||
class Secret(BaseModel):
|
class Secret(BaseModel):
|
||||||
|
id: int = None
|
||||||
user_id: int = None
|
user_id: int = None
|
||||||
data: str
|
data: str
|
||||||
salt: str = None
|
|
||||||
notes: str = None
|
|
||||||
added_on: str = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
added_on: str = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
modified_on: str = None
|
modified_on: str = None
|
||||||
active: bool = True
|
active: bool = True
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user