Added libs
This commit is contained in:
17
lockbox/frontend/src/App.vue
Normal file
17
lockbox/frontend/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import Home from './views/Home.vue';
|
||||
</script>
|
||||
|
||||
|
||||
<!-- TODO Create API folder -->
|
||||
<template>
|
||||
<div class="flex h-screen">
|
||||
<Home/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(body) {
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
||||
1
lockbox/frontend/src/assets/logo.svg
Normal file
1
lockbox/frontend/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
1
lockbox/frontend/src/assets/main.css
Normal file
1
lockbox/frontend/src/assets/main.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
29
lockbox/frontend/src/components/buttons/Button.vue
Normal file
29
lockbox/frontend/src/components/buttons/Button.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||
disabled
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-white text-black hover:bg-yellow-400',
|
||||
className
|
||||
]"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button'
|
||||
},
|
||||
disabled: Boolean,
|
||||
className: String
|
||||
})
|
||||
|
||||
defineEmits(['click'])
|
||||
</script>
|
||||
37
lockbox/frontend/src/components/buttons/ToogleSwitch.vue
Normal file
37
lockbox/frontend/src/components/buttons/ToogleSwitch.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<button
|
||||
:aria-pressed="modelValue"
|
||||
role="switch"
|
||||
:aria-checked="modelValue"
|
||||
@click="toggle"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
|
||||
modelValue ? 'bg-yellow-400' : 'bg-gray-300 dark:bg-gray-600',
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : ''
|
||||
]"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
modelValue ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
disabled: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const toggle = () => {
|
||||
if (!props.disabled) {
|
||||
emit('update:modelValue', !props.modelValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
12
lockbox/frontend/src/components/cards/Card.vue
Normal file
12
lockbox/frontend/src/components/cards/Card.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rounded-xl border"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
9
lockbox/frontend/src/components/cards/CardContent.vue
Normal file
9
lockbox/frontend/src/components/cards/CardContent.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="p-4 pt-0" :class="$attrs.class">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
12
lockbox/frontend/src/components/cards/CardDescription.vue
Normal file
12
lockbox/frontend/src/components/cards/CardDescription.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<p
|
||||
class="text-sm text-muted-foreground text-gray-500 dark:text-gray-400"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<slot></slot>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
9
lockbox/frontend/src/components/cards/CardFooter.vue
Normal file
9
lockbox/frontend/src/components/cards/CardFooter.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="flex items-center p-4 pt-0" :class="$attrs.class">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
9
lockbox/frontend/src/components/cards/CardHeader.vue
Normal file
9
lockbox/frontend/src/components/cards/CardHeader.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col space-y-1.5 p-4 mb-3" :class="$attrs.class">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
12
lockbox/frontend/src/components/cards/CardTitle.vue
Normal file
12
lockbox/frontend/src/components/cards/CardTitle.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<h3
|
||||
class="font-semibold text-2xl leading-none tracking-tight"
|
||||
:class="$attrs.class"
|
||||
>
|
||||
<slot></slot>
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ inheritAttrs: false })
|
||||
</script>
|
||||
50
lockbox/frontend/src/components/inputs/InputText.vue
Normal file
50
lockbox/frontend/src/components/inputs/InputText.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
v-bind="$attrs"
|
||||
:id="id"
|
||||
:type="showPassword ? 'text' : type"
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'w-full pr-10 rounded-md border border-gray-300 px-3 py-2 text-sm text-black shadow-sm focus:outline-none focus:ring-1 focus:ring-yellow-400 focus:border-yellow-400 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
error ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''
|
||||
]"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="type === 'password'"
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute inset-y-0 right-0 flex items-center px-2 text-gray-500"
|
||||
tabindex="-1"
|
||||
>
|
||||
<EyeIcon v-if="showPassword" class="h-4 w-4"></EyeIcon>
|
||||
<EyeSlashIcon v-else class="h-4 w-4"></EyeSlashIcon>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/solid'
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
modelValue: String,
|
||||
id: String,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
placeholder: String,
|
||||
disabled: Boolean,
|
||||
error: Boolean
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const showPassword = ref(false)
|
||||
</script>
|
||||
12
lockbox/frontend/src/components/labels/Label.vue
Normal file
12
lockbox/frontend/src/components/labels/Label.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<label :for="forId" :class="['block text-sm font-medium text-gray-700 dark:text-gray-300', className]">
|
||||
<slot></slot>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
forId: String,
|
||||
className: String
|
||||
})
|
||||
</script>
|
||||
125
lockbox/frontend/src/components/tabs/TabCreateUser.vue
Normal file
125
lockbox/frontend/src/components/tabs/TabCreateUser.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { TabPanel} from '@headlessui/vue'
|
||||
import * as secp from '@noble/secp256k1'
|
||||
import { KeyIcon, ArrowPathIcon, ClipboardDocumentIcon } from '@heroicons/vue/24/solid'
|
||||
|
||||
import Card from '../cards/Card.vue';
|
||||
import CardHeader from '../cards/CardHeader.vue';
|
||||
import CardTitle from '../cards/CardTitle.vue';
|
||||
import CardDescription from '../cards/CardDescription.vue';
|
||||
import CardContent from '../cards/CardContent.vue';
|
||||
import Label from '../labels/Label.vue';
|
||||
import InputText from '../inputs/InputText.vue';
|
||||
import Button from '../buttons/Button.vue';
|
||||
import ToogleSwitch from '../buttons/ToogleSwitch.vue';
|
||||
|
||||
const privateKeyInput = ref("")
|
||||
const privateKeyB64 = ref("")
|
||||
const publicKeyB64 = ref("")
|
||||
const isTextEnabled = ref(true)
|
||||
|
||||
async function generate() {
|
||||
var array = null
|
||||
if (isTextEnabled.value){
|
||||
const encoder = new TextEncoder();
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(privateKeyInput.value));
|
||||
array = new Uint8Array(hashBuffer);
|
||||
|
||||
privateKeyB64.value = btoa(String.fromCharCode(...array))
|
||||
}else{
|
||||
try {
|
||||
const decoded = atob(privateKeyInput.value)
|
||||
const bytes = new Uint8Array(decoded.length)
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i)
|
||||
}
|
||||
if (bytes.length !== 32) {
|
||||
throw new Error('Tamanho inválido')
|
||||
}
|
||||
array = bytes
|
||||
} catch (e) {
|
||||
array = new Uint8Array(32)
|
||||
crypto.getRandomValues(array)
|
||||
privateKeyInput.value = btoa(String.fromCharCode(...array))
|
||||
}
|
||||
}
|
||||
generatePublicKey(array)
|
||||
|
||||
}
|
||||
|
||||
function generatePublicKey(privateKeyBytes) {
|
||||
const publicKeyBytes = secp.getPublicKey(privateKeyBytes)
|
||||
publicKeyB64.value = btoa(String.fromCharCode(...publicKeyBytes))
|
||||
}
|
||||
|
||||
function onSwitchChanged(newValue) {
|
||||
privateKeyInput.value = ""
|
||||
privateKeyB64.value = ""
|
||||
publicKeyB64.value = ""
|
||||
}
|
||||
|
||||
function copy(text){
|
||||
navigator.clipboard.writeText(text)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
<KeyIcon class="h-5 w-5" />
|
||||
Create New User
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Generate a new ECDSA keypair for user authentication
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class=" space-y-2">
|
||||
<!-- Private key Characteres input -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<Label>Private Key</Label>
|
||||
<ToogleSwitch v-model="isTextEnabled" @update:modelValue="onSwitchChanged"/>
|
||||
<Label>Text Password</Label>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="privateKey" type="password" v-model="privateKeyInput" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
<Button @click="generate()" class="gap-2">
|
||||
<ArrowPathIcon class="h-4 w-4"/>
|
||||
<span v-if="!privateKeyInput">Generate</span>
|
||||
</Button>
|
||||
<Button v-if="privateKeyInput" @click="copy(privateKeyInput)" class="flex items-center gap-2">
|
||||
<ClipboardDocumentIcon class="h-4 w-4"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Public Key -->
|
||||
<div class="space-y-2">
|
||||
<Label forId="publicKey" class="text-slate-300">Public Key</Label>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="publicKey" type="text" v-model="publicKeyB64" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
<Button @click="copy(publicKeyB64)" class="flex items-center gap-2">
|
||||
<ClipboardDocumentIcon class="h-4 w-4"/>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Private Key -->
|
||||
<div class="space-y-2" v-if="isTextEnabled">
|
||||
<Label forId="privateKeyB64" class="text-slate-300">Private Key</Label>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="privateKeyB64" type="password" v-model="privateKeyB64" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
<Button @click="copy(privateKeyB64)" class="flex items-center gap-2">
|
||||
<ClipboardDocumentIcon class="h-4 w-4"/>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</template>
|
||||
245
lockbox/frontend/src/components/tabs/TabProofOfWork.vue
Normal file
245
lockbox/frontend/src/components/tabs/TabProofOfWork.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script setup>
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
|
||||
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
|
||||
|
||||
import Card from '../cards/Card.vue';
|
||||
import CardHeader from '../cards/CardHeader.vue';
|
||||
import CardTitle from '../cards/CardTitle.vue';
|
||||
import CardDescription from '../cards/CardDescription.vue';
|
||||
import CardContent from '../cards/CardContent.vue';
|
||||
import Label from '../labels/Label.vue';
|
||||
import InputText from '../inputs/InputText.vue';
|
||||
import Button from '../buttons/Button.vue';
|
||||
import CardFooter from '../cards/CardFooter.vue';
|
||||
import { ref } from 'vue';
|
||||
import socket from '@/plugins/socketio'
|
||||
|
||||
const publicKey = ref("")
|
||||
const targetDifficulty = ref("4")
|
||||
|
||||
const lockboxServiceApiMineUrl = "http://127.0.0.1:5001/mine";
|
||||
const tasks = ref(null)
|
||||
|
||||
// TODO Create api file
|
||||
|
||||
function start_mining(){
|
||||
const data = {"public_key":publicKey.value, "force":targetDifficulty.value};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiMineUrl+"/start", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
list_tasks()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function list_tasks(){
|
||||
fetch(lockboxServiceApiMineUrl+"/list")
|
||||
.then(response => {
|
||||
if (!response.ok){
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
tasks.value = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function pause_task(taskId){
|
||||
const data = {};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiMineUrl+"/pause/"+taskId, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
list_tasks()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function resume_task(taskId){
|
||||
const data = {};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiMineUrl+"/resume/"+taskId, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
list_tasks()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function cancel_task(taskId){
|
||||
const data = {};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiMineUrl+"/cancel/"+taskId, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
list_tasks()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('taskUpdated', function(msg) {
|
||||
tasks.value[msg.task_id] = msg.result
|
||||
});
|
||||
|
||||
list_tasks()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel class=" space-y-5">
|
||||
<!-- Mining user -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<BoltIcon class="h-5 w-5" />
|
||||
Proof of Work
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Generate proof of work for a public key
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div class="flex flex-row w-full space-x-5 items-center justify-center text-center">
|
||||
<div class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="publicKey" class="text-slate-300">Public Key</Label>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="publicKey" v-model="publicKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="targetDifficulty" class="text-slate-300">Target Difficulty (Leading Zeros)</Label>
|
||||
<div class="flex gap-2">
|
||||
<InputText id="targetDifficulty" v-model="targetDifficulty" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button @click="start_mining()" class="flex items-center gap-2 w-full">
|
||||
Start Mining
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<!-- Active jobs -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
Active Jobs
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Monitor and control proof of work jobs
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="overflow-y-auto rounded-md p-4">
|
||||
<CardContent v-for="([taskId, task]) in Object.entries(tasks)" :key="taskId" class="border border-slate-300 rounded mb-2 pt-2">
|
||||
<div class="grid md:grid-cols-5 gap-4 items-center">
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-gray-300 w-full">Public Key</p>
|
||||
<p class=" text-gray-300">{{task.public_key}}</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-gray-300 w-full">Difficulty</p>
|
||||
<p class=" text-gray-300">{{task.best_force}}/{{task.target_force}}</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-gray-300 w-full">Nonce</p>
|
||||
<p class=" text-gray-300">{{task.best_nonce}}</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-gray-300 w-full">Status</p>
|
||||
<p :class="{
|
||||
'text-yellow-400': task.status === 'paused',
|
||||
'text-blue-400': task.status === 'running',
|
||||
'text-green-400': task.status === 'completed',
|
||||
'text-red-400': task.status === 'cancelled',
|
||||
'text-gray-300': !['paused', 'running', 'completed', 'cancelled'].includes(task.status)
|
||||
}">
|
||||
{{ task.status }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-3 items-center mt-2">
|
||||
<div v-if="!['completed', 'cancelled', 'error'].includes(task.status)" class="space-x-3">
|
||||
<!-- TODO Change to Icons -->
|
||||
<Button v-if="['running'].includes(task.status)" @click="pause_task(taskId)">Pause</Button>
|
||||
<Button v-if="['paused'].includes(task.status)" @click="resume_task(taskId)">Playy</Button>
|
||||
<Button @click="cancel_task(taskId)">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</TabPanel>
|
||||
</template>
|
||||
126
lockbox/frontend/src/components/tabs/TabSigningRequests.vue
Normal file
126
lockbox/frontend/src/components/tabs/TabSigningRequests.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
|
||||
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
|
||||
|
||||
import Card from '../cards/Card.vue';
|
||||
import CardHeader from '../cards/CardHeader.vue';
|
||||
import CardTitle from '../cards/CardTitle.vue';
|
||||
import CardDescription from '../cards/CardDescription.vue';
|
||||
import CardContent from '../cards/CardContent.vue';
|
||||
import { ref } from 'vue';
|
||||
import Button from '../buttons/Button.vue';
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import socket from '@/plugins/socketio'
|
||||
|
||||
const lockboxServiceApiUrl = "http://127.0.0.1:5001";
|
||||
const auth = useAuthStore()
|
||||
|
||||
const requestedSignatures = ref([])
|
||||
|
||||
function listSignatureRequests(user){
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: {'Authorization': 'Bearer ' + auth.tokens[user]}
|
||||
};
|
||||
fetch(lockboxServiceApiUrl+"/users/"+user+"/signatures", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok){
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
requestedSignatures.value = data
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function approveSignature(requestId, user, approved){
|
||||
const data = {"approved":approved};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + auth.tokens[user]},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiUrl+"/users/"+user+"/signatures/"+requestId, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
updateSignatures()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('userAdded', function(msg) {
|
||||
updateSignatures()
|
||||
});
|
||||
|
||||
socket.on('signatureWaiting', function(msg) {
|
||||
updateSignatures()
|
||||
});
|
||||
|
||||
function updateSignatures(){
|
||||
Object.entries(auth.tokens).forEach(([key, token]) => {
|
||||
listSignatureRequests(key)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel>
|
||||
<!-- Head -->
|
||||
<Card >
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
<ClipboardDocumentListIcon class="h-5 w-5" />
|
||||
Pending Signing Requests
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Review and approve signing requests from logged-in users
|
||||
</CardDescription>
|
||||
<Button @click="updateSignatures()">Refresh</Button>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<CardContent v-for="request in requestedSignatures" class="border border-slate-300 rounded mb-2 pt-2">
|
||||
<div class="flex flex-col ">
|
||||
<div class="flex flex-col w-full">
|
||||
<p class="font-mono text-sm text-gray-300">{{request.requestId}}</p>
|
||||
<!-- <div class="flex flex-row space-y-1 gap-2">
|
||||
<p class="font-mono text-sm text-gray-300">{{request.moduleRequesting}}</p>
|
||||
<p class="font-mono text-sm text-gray-300">{{request.requestedAt}}</p>
|
||||
</div> -->
|
||||
<p class="font-mono text-sm text-gray-300">{{request.publicKey}}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span>{{request.data}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-x-3 items-center">
|
||||
<p class="font-mono text-sm text-gray-300">{{request.action}}</p>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button @click="approveSignature(request.requestId, request.user, true)">Approve</Button>
|
||||
<Button @click="approveSignature(request.requestId, request.user, false)">Reject</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</TabPanel>
|
||||
</template>
|
||||
201
lockbox/frontend/src/components/tabs/TabUsers.vue
Normal file
201
lockbox/frontend/src/components/tabs/TabUsers.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<script setup>
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
|
||||
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
import Card from '../cards/Card.vue';
|
||||
import CardHeader from '../cards/CardHeader.vue';
|
||||
import CardTitle from '../cards/CardTitle.vue';
|
||||
import CardDescription from '../cards/CardDescription.vue';
|
||||
import CardContent from '../cards/CardContent.vue';
|
||||
import Label from '../labels/Label.vue';
|
||||
import ToogleSwitch from '../buttons/ToogleSwitch.vue';
|
||||
import { ref } from 'vue';
|
||||
import InputText from '../inputs/InputText.vue';
|
||||
import CardFooter from '../cards/CardFooter.vue';
|
||||
import Button from '../buttons/Button.vue';
|
||||
|
||||
const privateKey = ref("")
|
||||
const publicKey = ref("")
|
||||
const proofOfWork = ref("")
|
||||
const credentialPassword = ref("")
|
||||
const isLoginEnabled = ref(false)
|
||||
|
||||
const lockboxServiceApiUrl = "http://127.0.0.1:5001";
|
||||
const auth = useAuthStore()
|
||||
|
||||
const users = ref({})
|
||||
|
||||
function onSwitchChanged(newValue) {
|
||||
// privateKeyInput.value = ""
|
||||
// privateKeyB64.value = ""
|
||||
// publicKeyB64.value = ""
|
||||
}
|
||||
|
||||
function set_login_user(userId){
|
||||
if(!isLoginEnabled.value){
|
||||
isLoginEnabled.value = true
|
||||
}
|
||||
publicKey.value = userId
|
||||
}
|
||||
|
||||
function add_user(){
|
||||
const data = {password:privateKey.value, data:{proof_of_work:proofOfWork.value}, credentialPassword:credentialPassword.value};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiUrl+"/users", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
auth.setToken(data.verifying_key, data.token);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function login_user(){
|
||||
const data = {credentialPassword:credentialPassword.value};
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
|
||||
fetch(lockboxServiceApiUrl+"/users/"+publicKey.value+"/login", requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
auth.setToken(data.verifying_key, data.token);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error
|
||||
|
||||
('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function logout_user(userId){
|
||||
delete auth.tokens[userId]
|
||||
list_users()
|
||||
}
|
||||
|
||||
function list_users(){
|
||||
fetch(lockboxServiceApiUrl+"/users")
|
||||
.then(response => {
|
||||
if (!response.ok){
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
users.value = data;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
list_users()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabPanel class="flex flex-col space-y-3">
|
||||
<Card class="">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
<UserPlusIcon class="h-5 w-5" />
|
||||
Add User
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
Add a user with private key or login with public key
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class=" space-y-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<Label>Add with Private Key</Label>
|
||||
<ToogleSwitch v-model="isLoginEnabled" @update:modelValue="onSwitchChanged"/>
|
||||
<Label>Login with Public Key</Label>
|
||||
</div>
|
||||
<div v-if="!isLoginEnabled" class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="privateKey" class="text-slate-300">Private Key</Label>
|
||||
<InputText id="privateKey" type="password" v-model="privateKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
<div v-if="!isLoginEnabled" class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="proofOfWork" class="text-slate-300">Proof of Work</Label>
|
||||
<InputText id="proofOfWork" v-model="proofOfWork" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
<div v-if="isLoginEnabled" class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="publicKey" class="text-slate-300">Public Key</Label>
|
||||
<InputText id="publicKey" v-model="publicKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-2 w-full">
|
||||
<Label forId="credentialPassword" class="text-slate-300">Credential Password</Label>
|
||||
<InputText id="credentialPassword" type="password" v-model="credentialPassword" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button v-if="!isLoginEnabled" @click="add_user()" class="flex items-center gap-2 w-full">
|
||||
Save
|
||||
</Button>
|
||||
<Button v-if="isLoginEnabled" @click="login_user()" class="flex items-center gap-2 w-full">
|
||||
Login
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card >
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2 text-yellow-400">
|
||||
<UsersIcon class="h-5 w-5" />
|
||||
User Management
|
||||
</CardTitle>
|
||||
<CardDescription class="text-slate-200">
|
||||
View and manage your users in the system
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent v-for="user in users" class="border border-slate-300 rounded mb-2 pt-2">
|
||||
<div class="flex flex-col ">
|
||||
<div class="space-y-1 w-full">
|
||||
<p class="font-mono text-sm text-gray-300">{{user.id}}</p>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||
<span>Added At: {{user.added_at}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-3 items-center">
|
||||
<div v-if="user.id in auth.tokens">
|
||||
<p class="">Logged In</p>
|
||||
<Button @click="logout_user(user.id)">Logout</Button>
|
||||
</div>
|
||||
<div v-if="!(user.id in auth.tokens)">
|
||||
<p class="">Logged Out</p>
|
||||
<Button @click="set_login_user(user.id)">Login</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</template>
|
||||
12
lockbox/frontend/src/main.js
Normal file
12
lockbox/frontend/src/main.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
import socket from './plugins/socketio'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.config.globalProperties.$socket = socket
|
||||
app.mount('#app')
|
||||
3
lockbox/frontend/src/plugins/socketio.js
Normal file
3
lockbox/frontend/src/plugins/socketio.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { io } from 'socket.io-client'
|
||||
const socket = io('https://localhost:5001')
|
||||
export default socket
|
||||
21
lockbox/frontend/src/stores/auth.js
Normal file
21
lockbox/frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
tokens: {},
|
||||
}),
|
||||
actions: {
|
||||
setToken(publicKey, token) {
|
||||
this.tokens[publicKey] = token
|
||||
localStorage.setItem(publicKey, token)
|
||||
},
|
||||
clearToken(publicKey) {
|
||||
this.tokens[publicKey] = null
|
||||
localStorage.removeItem(publicKey)
|
||||
},
|
||||
loadTokenFromStorage(publicKey) {
|
||||
const token = localStorage.getItem(publicKey)
|
||||
if (token) this.tokens[publicKey] = token
|
||||
}
|
||||
},
|
||||
})
|
||||
67
lockbox/frontend/src/views/Home.vue
Normal file
67
lockbox/frontend/src/views/Home.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
|
||||
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Card from '@/components/cards/Card.vue'
|
||||
import CardHeader from '@/components/cards/CardHeader.vue'
|
||||
import CardTitle from '@/components/cards/CardTitle.vue'
|
||||
import CardDescription from '@/components/cards/CardDescription.vue'
|
||||
import CardContent from '@/components/cards/CardContent.vue'
|
||||
import CardFooter from '@/components/cards/CardFooter.vue'
|
||||
import TabCreateUser from '@/components/tabs/TabCreateUser.vue'
|
||||
import TabProofOfWork from '@/components/tabs/TabProofOfWork.vue'
|
||||
import TabAddUser from '@/components/tabs/TabUsers.vue'
|
||||
import TabSigningRequests from '@/components/tabs/TabSigningRequests.vue'
|
||||
|
||||
const tabItems = [
|
||||
{label:"Create User", icon:KeyIcon, tabComponent:TabCreateUser},
|
||||
{label:"Proof of Work", icon:BoltIcon, tabComponent:TabProofOfWork},
|
||||
{label:"Users", icon:UserPlusIcon, tabComponent:TabAddUser},
|
||||
{label:"Signing Requests", icon:ClipboardDocumentListIcon, tabComponent:TabSigningRequests},
|
||||
]
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-black w-screen text-yellow-400">
|
||||
<header className="border-b border-yellow-400/20 py-6">
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="text-3xl font-black">
|
||||
<span className="text-yellow-400">LOCK</span>
|
||||
<span className="text-white">BOX</span>
|
||||
</div>
|
||||
<div className="w-1 h-8 bg-yellow-400"></div>
|
||||
<span className="text-gray-400">Secure ECDSA Keypair Management</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
<TabGroup>
|
||||
<TabList class="flex flex-wrap gap-2 mb-8 border-b border-yellow-400/20">
|
||||
<Tab v-for="tab in tabItems" as="template" :key="tab" v-slot="{ selected }">
|
||||
<button class="flex items-center space-x-2 px-6 py-3 font-semibold transition-all duration-300 border-b-2"
|
||||
:class="{ 'text-yellow-400 border-yellow-400': selected, 'text-gray-400 border-transparent hover:text-yellow-400 hover:border-yellow-400/50': !selected }">
|
||||
<component :is="tab.icon" class="h-4 w-4"></component>
|
||||
<span>{{ tab.label }}</span>
|
||||
</button>
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels class="mt-6 space-y-6 px-40" v-for="tab in tabItems" :key="tab.label">
|
||||
<component :is="tab.tabComponent"></component>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* button {
|
||||
transition: background 0.2s, color 0.2s;
|
||||
} */
|
||||
</style>
|
||||
Reference in New Issue
Block a user