Compare commits

..

No commits in common. "master" and "50094eb3c8dfe27551c056ed60bc2ee0cb488fd0" have entirely different histories.

16 changed files with 41 additions and 687 deletions

View File

@ -9,7 +9,6 @@
"version": "0.0.0",
"dependencies": {
"element-plus": "^2.7.5",
"totp-generator": "^1.0.0",
"vue": "^3.4.21"
},
"devDependencies": {
@ -1831,14 +1830,6 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"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": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -2370,14 +2361,6 @@
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"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": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",

View File

@ -12,7 +12,6 @@
},
"dependencies": {
"element-plus": "^2.7.5",
"totp-generator": "^1.0.0",
"vue": "^3.4.21"
},
"devDependencies": {

View File

@ -1,48 +1,12 @@
<script setup>
import CreateSecret from "./components/CreateSecret.vue";
import HomePage from "./components/HomePage.vue";
import ListSecrets from "./components/ListSecrets.vue";
import HelloWorld from "./components/HelloWorld.vue";
</script>
<template>
<div>
<div id="header" class="header">
<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>
<HelloWorld msg="You did it!" @loggedin="loggedin = true" />
<CreateSecret v-if="loggedin" />
</div>
</template>
@ -51,157 +15,8 @@ export default {
data() {
return {
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>
<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>
<style scoped></style>

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,5 +1,8 @@
<template>
<div>
<h3>
{{ title }}
</h3>
<div id="container">
<el-form :model="form" label-width="auto">
<el-form-item label="Issuer">
@ -15,84 +18,43 @@
<el-input v-model="form.notes" />
</el-form-item>
</el-form>
<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
>
<el-button @click="createSecret" type="primary">Create secret</el-button>
</div>
</div>
</template>
<script>
export default {
props: {
editSecret: Object,
},
data() {
return {
title: "Create Secret",
apiBaseUrl: "http://localhost:8000",
id: null,
form: {
issuer: "",
username: "",
secret: "",
notes: "",
issuer: "asdfasdf",
username: "asdfasdf",
secret: "asdfasdf",
notes: "asdfasdf",
},
method: "POST",
showAddMore: true,
};
},
methods: {
async createSecret() {
const url = `${this.apiBaseUrl}/secret`;
const token = sessionStorage.getItem("token");
const bodyData = { data: btoa(JSON.stringify(this.form)) };
if (this.method === "PUT") {
bodyData["id"] = this.id;
}
const token = localStorage.getItem("token");
const requestOptions = {
method: this.method,
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(bodyData),
body: JSON.stringify({ data: btoa(JSON.stringify(this.form)) }),
};
console.log(requestOptions);
await fetch(url, requestOptions)
.then((response) => response.json())
.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>

View File

@ -1,3 +1,12 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
});
</script>
<template>
<div>
<el-form :model="form" label-width="auto">
@ -49,7 +58,7 @@ export default {
if ("message" in response) {
if (response.message === "authenticated") {
const token = response.accessToken;
sessionStorage.setItem("token", token);
localStorage.setItem("token", token);
this.$emit("loggedin", true);
}
}

View File

@ -1,234 +1,13 @@
<template>
<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>
<div></div>
</template>
<script>
import { TOTP } from "totp-generator";
import SecretCard from "./SecretCard.vue";
export default {
components: { SecretCard },
data() {
return {
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>
<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>

View File

@ -1,144 +0,0 @@
<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> &#8226;&#8226;&#8226;&#8226;&#8226;&#8226; </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>

56
main.py
View File

@ -123,7 +123,9 @@ async def login(user: UserLogin):
@app.post("/secret")
async def create_secret(secret: Secret, current_user: dict = Depends(get_current_user)):
"""
Stores an encrypted secret for the user.
Stores and encrypted secret for the user.
The encrypted secret is unreadable on the server and is encrypted on the front-end
"""
data = []
@ -131,12 +133,6 @@ async def create_secret(secret: Secret, current_user: dict = Depends(get_current
text = f.read()
if 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']
encryption_key = current_user['encryption_key'].encode()
@ -150,40 +146,6 @@ async def create_secret(secret: Secret, current_user: dict = Depends(get_current
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')
async def list_secret(current_user: dict = Depends(get_current_user)):
"""Returns the encrypted secrets of the user."""
@ -197,20 +159,10 @@ async def list_secret(current_user: dict = Depends(get_current_user)):
user_id = current_user['id']
encryption_key = current_user['encryption_key'].encode()
user_secrets = [i for i in data if i['user_id']==user_id and i['active']]
user_secrets = [i for i in data if i['user_id']==user_id]
for secret in user_secrets:
cur_data = secret['data']
decrypted_data = fernet_decrypt(cur_data, encryption_key)
secret['data'] = decrypted_data
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)

View File

@ -17,9 +17,10 @@ class UserLogin(BaseModel):
password: str
class Secret(BaseModel):
id: int = None
user_id: int = None
data: str
salt: str = None
notes: str = None
added_on: str = datetime.datetime.now(datetime.timezone.utc).isoformat()
modified_on: str = None
active: bool = True

View File

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