Added libs

This commit is contained in:
Lucas
2026-01-25 13:55:46 +10:00
parent 575c682afc
commit f70af3c4ea
229 changed files with 26983 additions and 0 deletions

30
lockbox/frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

View File

@@ -0,0 +1,29 @@
# vue
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

19
lockbox/frontend/ca.pem Normal file
View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDEzCCAfugAwIBAgIULnlTx9ReaTKzpVwe2WsLQUVqDiIwDQYJKoZIhvcNAQEL
BQAwOTELMAkGA1UEBhMCVVMxETAPBgNVBAoMCE5vU3lzLUNBMRcwFQYDVQQDDA5O
b1N5cyBMb2NhbCBDQTAeFw0yNTA5MDYwODIzMDdaFw0zNTA5MDQwODIzMDdaMDkx
CzAJBgNVBAYTAlVTMREwDwYDVQQKDAhOb1N5cy1DQTEXMBUGA1UEAwwOTm9TeXMg
TG9jYWwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9RQvCGPLZ
z8V4qhM1IsUmSP3/05jEd9oBN5AmXIdiFHjlzBGVwgaqvhX7dWMAlaodN5s3moCu
His2NyhCikXv/MepkguWChsljmfqiVEUUN/ZVYaUzYwYMQ7KY1Pz1+qaB6dsOzsi
RFWv0AOZ5er3AIenpRF/3Y5yL3yqwzNcgEr5ULppCvIJdtyt9Da43p0zFnphGQL0
IIAUnp4mEyBypghMWsNn2RtnD7gxGFrcHWgilC+EG44MUGnDZ2jAdxOc/x8Mac/n
/VGCAYEGWIK/3lsAK1vdZMOQAXNpZC8zsPJFwNUXWQoJmZoqb9xyYmHU9VH/oc24
wt5Q6S1/MYJlAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAKEm4Jv1KGkZJHl/nnalpbxoc6Rj2KsaWvsGgW4NyGk+rBKqfy9uhd9e
5DHoJ/KpLqQBL56+rU4oWu54/0CD0943oWoy6qroAphkJ3yhuRndKJgWvZHAyoIp
+ESzHkiYaf7UXMqO6zdC6CP1fJL6ap4eRokEhp4+y+g63KJowuZl6HKqNh6wQaW9
MqflZOP0rLBqDvbOPpSWUDZ77UKE4ZIRxarIas3KYyQtSBGBQjNXTysa7aCljGGv
yNhl3gQ7HZzchigPUtsdLgQRDI/wDcrwPPhlHfyy76M8gcP5Nsodn++rh1O+L+Ff
FCc+hSyCF6bajrkPWq/8WDCzC/XQs8k=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAvUULwhjy2c/FeKoTNSLFJkj9/9OYxHfaATeQJlyHYhR45cwR
lcIGqr4V+3VjAJWqHTebN5qArh4rNjcoQopF7/zHqZILlgobJY5n6olRFFDf2VWG
lM2MGDEOymNT89fqmgenbDs7IkRVr9ADmeXq9wCHp6URf92Oci98qsMzXIBK+VC6
aQryCXbcrfQ2uN6dMxZ6YRkC9CCAFJ6eJhMgcqYITFrDZ9kbZw+4MRha3B1oIpQv
hBuODFBpw2dowHcTnP8fDGnP5/1RggGBBliCv95bACtb3WTDkAFzaWQvM7DyRcDV
F1kKCZmaKm/ccmJh1PVR/6HNuMLeUOktfzGCZQIDAQABAoIBAAZ2GB5IHcGoujt0
Y4dSUANJu7PvjO0FBwlgD63PlIkjBGVCEQsAdMMxvyKFVDmD4nmryoe6G7NCTrVX
VmWq0k60DwBFplGbVVSHvQYgoEHjhiR+F1ex4ySybs/UQys1W7ZmlUKus+OO5OH2
yDPEZzYRdZAYGr+tk02PY3OZO25U2HbS7xZWz68xWQJPuEJLm4kckWX+ylv0vlEM
H7XcdNlAizGBp8wazQDL7kdfhnxZKUDTyaR4iWqC4qDT5bObPU4t7xiE43W8DH6C
DI22zxriSeAkjzRbFAv67ws+nEzDvm7Oshz0kD9WqK0U0nkcibrCLXckZHni4ewQ
HKzCAq0CgYEA4Tcpve20GhyCngEnk+MpUs7FP686+Ytrr7P65yRXEO8H1LPInD6J
w/KRP5cRtCgV/Q+pp6yTr5YQlSSR7evQy04pquEJ6423n4zQTjROpmGxIVNzeAv+
JwWpInLbEe5iafds+EXlAEo1buB5J47/LLP7LLcTOTtmLzjZzIPczgMCgYEA1yQN
k6yPx5AHqjXSSH3UK0QC/lIMDBHbKtk+eB6lobN/skY9Ggw41Ph5wOZlJU4VQ9q9
CsPtHr6izuxezmzbV0ae6a7dxM5fCSVs6V7IGNGIKF1xk6J0dqRbQxjv9Cid4mZM
ORDNMdDWnfiB+/hjJ8KbLxZVJeYG7rXzkZvilXcCgYAUTXjB2m/l+rP7snby6gOL
p4A4oX9bh6oJiNwRgkEnEaVPE3X+P9UDiRZ2+RNrfkGdMpBEwVX++jQ8fbN6E0wb
R8yRzv+p8HihNXyB0E1Wym/BZVh/dfVPZz88D8aX8zmD+/4i04o1YHs4p5vEaSuv
x/nYqhhdjHFFyIY53ZlGKQKBgGItRrDUN4y3Mng/NWX1XeQclk0effboEx77omFI
gwdGlYhyOyHu3+R4O3+G5DNg7Z1YbZpDDKtSDZPmE+GZlYK/bHdxYCyWjJHs7UWW
BjQlMkBRXComIYWevTLiZ2YBPwN48WG9RBZumfHe3NpyYDVdvll/lH3F+fXnPG5/
N6hDAoGARq6gWGk4IiZj9J0P8ZgKGZzWwnxcAxPm5JbbkhTGNtrkJMOAok6j/vDy
dq5i6XWXQg6jXlC8q6sNQfRjCZEzpdxlzsjpmeSMoKaqzTyQVTUQcdUtHedSPUDI
YyXJScUDIfGaRgC8UwF8c4vpYPmy1s+jGD3CyhMLM8kgeM+85Yc=
-----END RSA PRIVATE KEY-----

19
lockbox/frontend/cert.pem Normal file
View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDFjCCAf6gAwIBAgIUSdT3UOQDfikwJWhlc2sxUE6Vsi4wDQYJKoZIhvcNAQEL
BQAwOTELMAkGA1UEBhMCVVMxETAPBgNVBAoMCE5vU3lzLUNBMRcwFQYDVQQDDA5O
b1N5cyBMb2NhbCBDQTAeFw0yNTA5MDYwODIzMDdaFw0zNTA5MDQwODIzMDdaMDEx
CzAJBgNVBAYTAlVTMQ4wDAYDVQQKDAVOb1N5czESMBAGA1UEAwwJbG9jYWxob3N0
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArg8PlencKCgepNGaRDx0
lr+2QdAzILSaPt8WNAYK9+cNHfUAhoWB2c47nZezKtE5tInrw/6Ubx0wIvm2IaWR
dN6TKUpBzrPru7hyxAGxKbefRVLhxt/O8rXxkJqBeCf5YByVZ4oGiEyeKyCt6umd
faLQMB3g7ZKGS6khjWayVCMNh2BMwdYZ8pEDvdms91hdtS6rQntM9foOA1vYmwBv
Z735NbymNGDEDHPx9lgBc6ZCL3gwNGvwEo/NNXubuYdZhwDtDytZTRfal1z1gE+7
4BcX01ZdkUgo8bs6Gp9b3JEY59ctHv2K9HdMRJ8imfVq11HwUaVMrF17YOt43j7Z
NwIDAQABox4wHDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcN
AQELBQADggEBAAC0HUlG0udKDtiZUHXQklDDGCw/07ohnoRwVQxba6FzIPe74ca6
j6Y7dKw9pXFy1h5scUyjS7Sl0AEkAalDh0wnedUwXPBtfrz5tzBUyOLJPtyFMCAV
JhXpCivwd++f0p3TtpDvDR6VQy79rqPqCwO6wQ4dx/62g9f2NKtTztNV5w74TEV0
Dx4OyFPAVoiEpqpTEuJ1v7HPYM8I4EcvE3jZSJDmzO1EhcxuJvUcJ1o1QBa5HW34
/hTvDDlg2w+++Zc8/VYVgt/kGpf7jjFaUml3jzNNpxm+KGYudb+pGNVyb/w6schj
FfKloHvLD2pAsCXo8VQ28VcaH42DEqeF10I=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lockbox</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

27
lockbox/frontend/key.pem Normal file
View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEArg8PlencKCgepNGaRDx0lr+2QdAzILSaPt8WNAYK9+cNHfUA
hoWB2c47nZezKtE5tInrw/6Ubx0wIvm2IaWRdN6TKUpBzrPru7hyxAGxKbefRVLh
xt/O8rXxkJqBeCf5YByVZ4oGiEyeKyCt6umdfaLQMB3g7ZKGS6khjWayVCMNh2BM
wdYZ8pEDvdms91hdtS6rQntM9foOA1vYmwBvZ735NbymNGDEDHPx9lgBc6ZCL3gw
NGvwEo/NNXubuYdZhwDtDytZTRfal1z1gE+74BcX01ZdkUgo8bs6Gp9b3JEY59ct
Hv2K9HdMRJ8imfVq11HwUaVMrF17YOt43j7ZNwIDAQABAoIBABoZyREGagydg4bc
pYDw/dyzL93rnhcb7ftakaZId7GX9KgW2rbRY1jpa5gkrOnRSRFxEyknTlPhMRw1
jOG7xbWcQL4S1A5ufX1/WbpZtJrYXapUFOYxHoPX07sG6D4/5E3My3ykvnkG4DsA
YgQVdxflZ8mnWVjWvYuv94eQLFKgVSIr6OtJYAhpofVY/qRHYjBg8RLgjZbmyp9R
QShlza0XHj4XZV5pXRrBWPWlF5ws7/OXHnCDxVq2xV1EQ9652HUTVtjPWiyfW4Ih
pZV9kuDU8HT54tFjoVhWrVY3+EFrV63Y+6uUWwKoB/P8jMtvo5QwSgl8pZL61YQD
RTo01oUCgYEA8TZ09fxkNgdH4XyQp+8FjdiBSlRx7XyXiE7dOfzxLaZ34fi85sgZ
tI0Aai8ouskJci9QoKk2WkcX8vY0cyiXbgXehCbPBdRnPSM7zDRtvURhaL40w/+G
VTcgJ/WIv/SwrmqL2jl56/ywrf/n+g1cN2bkCpNDIyXYY5wj34/e9mMCgYEAuLq0
X1lzGzbgZ99XRNinCxq8hw7KVZpGd21bZsopgz7tnlZ4v347iIUhnNN93DwffFr8
nhoVo5acW9DP8/xb7cXU9D3jdR1mLPW1PwtGuxc9kBYFYXpaZ7CngT+B5NjKn4Vd
hNGIm2ltl3trRPGAmNCRXusG+ZC4ea3coKBqUB0CgYAOrobd9hfPZhAM/Hz9i8Hl
yVjNQmiQ0PWUOWCjx+6SHcDMQ0yUK3fNEowE6ovrGpN1nMWmkcYaJpuhkTTOEZlt
+/N4TbhqHWyPPxbDrilDzOa07mbdyy7M/wb5B6vkKyuZ4ihTBw6Ru5axcJMZGDkV
sjCNKDt85y/NmFJiqColCwKBgQCvERnxpxcEOoyPREUzVNNyHaN/p0+vsqaHdhcC
IiMXY+LThQWoDRykc+737iLAPiZktuHjf7r0Lr798LWzd30zqKH52lEe4366qx1a
ovgkRJEuZQAycj8NN4h3X9VdKOtWJJENV3pMNq0Ku4dcbjc+G6M5Pil9CF8byd5m
R8CZLQKBgQCieaqXoXI3PwuchtOp6rXmeMCmC669ukJsbqnlVQpD5Lr9gbNeyIZT
mjx643a5zAgyQ8u/EH9aXKB/iUUgAbNrY0dx09tlRqg361XSZDjzORIJRYqfaS6R
rX3PFqkkQ8ioCiS6TL2f+wFSCgNIzsZv1B+ScGQYzrvB7n/idVY/4A==
-----END RSA PRIVATE KEY-----

3778
lockbox/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"name": "vue",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0",
"@noble/secp256k1": "^2.3.0",
"pinia": "^3.0.3",
"socket.io-client": "^4.8.1",
"vue": "^3.5.17"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.11",
"@vitejs/plugin-vue": "^6.0.0",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11",
"vite": "^7.0.0",
"vite-plugin-vue-devtools": "^7.7.7"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>

View 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

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View 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>

View 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>

View File

@@ -0,0 +1,12 @@
<script setup>
defineOptions({ inheritAttrs: false })
</script>
<template>
<div
class="rounded-xl border"
:class="$attrs.class"
>
<slot></slot>
</div>
</template>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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')

View File

@@ -0,0 +1,3 @@
import { io } from 'socket.io-client'
const socket = io('https://localhost:5001')
export default socket

View 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
}
},
})

View 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>

View File

@@ -0,0 +1,23 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
tailwindcss(),
],
server: {
port: 3001,
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})

View File

@@ -0,0 +1,52 @@
import os
import webview
import os
import platform
import subprocess
def run_webview():
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(certfile=cert_path, keyfile=key_path)
url = "https://localhost:5001/"
webview.create_window(
"Unlock Key",
url,
width=800,
height=800,
resizable=True,
confirm_close=True,
text_select=True,
frameless=False,
background_color="#FFFFFF",
)
webview.start()
if __name__ == "__main__":
dir = os.path.dirname(os.path.abspath(__file__))
ca_path = os.path.join(dir, "ca.pem")
ca_key_path = os.path.join(dir, "ca_key.pem")
cert_path = os.path.join(dir, "cert.pem")
key_path = os.path.join(dir, "key.pem")
if not os.path.exists(cert_path) or not os.path.exists(key_path) or not os.path.exists(ca_path) or not os.path.exists(ca_key_path) :
ca, cert, key = generate_ca_and_cert(ca_path, ca_key_path, cert_path, key_path)
system = platform.system()
if system == "Windows":
add_ca_windows(ca)
elif system == "Darwin":
add_ca_macos(ca)
elif system == "Linux":
add_ca_linux(ca)
else:
raise Exception("OS not supported")
print("Cert installed")
else:
print("Cert already exists")
run_webview()