236 lines
No EOL
11 KiB
Vue
236 lines
No EOL
11 KiB
Vue
<template>
|
||
<div class="main d-flex w-100">
|
||
<div class="container d-flex flex-column">
|
||
<div class="row vh-100">
|
||
<div class="col-sm-10 col-md-8 col-lg-6 mx-auto d-table h-100">
|
||
<div class="d-table-cell align-middle">
|
||
<div class="text-center mt-4">
|
||
<h1 class="h2">
|
||
Toolshed
|
||
</h1>
|
||
|
||
<p class="lead" v-if="msg">
|
||
{{ msg }}
|
||
</p>
|
||
<p class="lead" v-else>
|
||
Create an account to get started
|
||
</p>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<div class="m-sm-4">
|
||
<div :class="errors.raw_record?['mb-3','is-invalid']:['mb-3']">
|
||
<label class="form-label">Identity Record</label>
|
||
<div class="input-group">
|
||
<input class="form-control form-control-lg" type="text"
|
||
:class="errors.raw_record?['is-invalid']:[]"
|
||
v-model="raw_record" placeholder="Paste identity record"
|
||
@input.prevent="try_parsing">
|
||
<span class="input-group-text form-control-lg" v-if="raw_record == ''"
|
||
@click="pasteClipboard()">
|
||
<b-icon-clipboard></b-icon-clipboard>
|
||
</span>
|
||
<span class="input-group-text form-control-lg" v-else
|
||
@click="raw_record = ''">
|
||
<b-icon-x-circle></b-icon-x-circle>
|
||
</span>
|
||
</div>
|
||
<div class="invalid-feedback">
|
||
{{ errors.raw_record }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3" v-if="is_unlocked || is_locked">
|
||
<label class="form-label">Username</label>
|
||
<div class="input-group">
|
||
<input class="form-control form-control-lg" type="email"
|
||
disabled v-model="username">
|
||
<span class="input-group-text form-control-lg bg-success"
|
||
v-if="is_unlocked">
|
||
<b-icon-unlock></b-icon-unlock>
|
||
</span>
|
||
<span class="input-group-text form-control-lg bg-warning"
|
||
v-if="is_locked">
|
||
<b-icon-lock></b-icon-lock>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3" v-if="is_locked" :class="errors.password?['is-invalid']:[]">
|
||
<form role="form" method="post" @submit.prevent="try_unlock">
|
||
<label class="form-label">Password</label>
|
||
<div class="input-group">
|
||
<input class="form-control form-control-lg" type="password"
|
||
name="password" placeholder="Enter your password"
|
||
v-model="password" :class="errors.password?['is-invalid']:[]">
|
||
<button class="btn btn-lg btn-primary" type="button"
|
||
@click="try_unlock">
|
||
<b-icon-unlock></b-icon-unlock>
|
||
unlock
|
||
</button>
|
||
</div>
|
||
<div class="invalid-feedback">
|
||
{{ errors.password }}
|
||
</div>
|
||
|
||
</form>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="form-check">
|
||
<input class="form-check-input" type="checkbox" value="remember-me"
|
||
name="remember-me" checked v-model="remember"
|
||
@change="setRemember(remember)">
|
||
<span class="form-check-label">
|
||
Remember me next time
|
||
</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="text-center mt-3">
|
||
<button type="submit" class="btn btn-lg btn-primary" :disabled="!is_unlocked"
|
||
@click="try_pairing">
|
||
Pair
|
||
</button>
|
||
</div>
|
||
<br/>
|
||
<div class="text-center">
|
||
<p class="mb-0 text-muted">
|
||
Already have an account?
|
||
<router-link to="/login">Login</router-link>
|
||
</p>
|
||
<p class="mb-0 text-muted">
|
||
Don’t have an account?
|
||
<router-link to="/register">Sign up</router-link>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import {mapActions, mapMutations} from 'vuex';
|
||
import {ServerSet, createSignAuth} from '../federation';
|
||
import NeighborsCache from '../neigbors';
|
||
import {decryptIdentityRecord, parseIdentityRecord, serializeIdentityRecord} from "@/identity";
|
||
import router from "@/router";
|
||
|
||
export default {
|
||
name: 'Pairing',
|
||
data() {
|
||
return {
|
||
msg: 'Pair with existing Account',
|
||
//raw_record: 'test_a@a.localhost:4ab79601edc400fd0263c7d5fd045ea7f5de28f1dd8905e2479f96893f3257d8:7f0ee42b',
|
||
//raw_record: 'test_a@a.localhost:$pbkdf2$100000$67da1c2a055dd41fc9707c37d2cdecea$8173b4d626352bcafab626b2$60027066142f55ecfd6f91b587f99c5f02ee536ce08c31e3f69148a51711489220a2b10d5450166ee09a781f891be8ab3a418e31a8e71f6468f189ba0baf31ece21166b63ba608683bdbea8afa7dfc33b1a994d8be4e4b2123',
|
||
raw_record: '',
|
||
crypted_record: null,
|
||
username: '',
|
||
password: '',
|
||
keypair: '',
|
||
remember: false,
|
||
errors: {}
|
||
}
|
||
},
|
||
computed: {
|
||
is_unlocked() {
|
||
return !!this.keypair && !!this.username;
|
||
},
|
||
is_locked() {
|
||
return !!this.username && !!this.crypted_record && !this.keypair;
|
||
}
|
||
},
|
||
methods: {
|
||
...mapActions(['lookupServer']),
|
||
...mapMutations(['setRemember', 'setUserFromIdentityRecord']),
|
||
async try_pairing() {
|
||
const unreachable = new NeighborsCache();
|
||
const servers = await this.lookupServer({username: this.username}).then(servers => new ServerSet(servers, unreachable))
|
||
const auth = createSignAuth(this.username, this.keypair.signSk);
|
||
const reply = await servers.getRaw(auth, '/api/v1/friends/');
|
||
if (reply.status === 200) {
|
||
this.setUserFromIdentityRecord({username: this.username, keypair: this.keypair});
|
||
router.push({path: '/'});
|
||
} else {
|
||
console.error('Error:', reply);
|
||
}
|
||
},
|
||
try_parsing() {
|
||
if (!this.raw_record) {
|
||
this.username = '';
|
||
this.keypair = '';
|
||
this.errors = {};
|
||
return;
|
||
}
|
||
const parsed = parseIdentityRecord(this.raw_record)
|
||
const {user, keypair, errors} = parsed;
|
||
if (user) {
|
||
this.username = user;
|
||
}
|
||
if (errors) {
|
||
this.errors.raw_record = errors;
|
||
}
|
||
if (keypair) {
|
||
this.keypair = keypair;
|
||
}
|
||
if (parsed.crypted) {
|
||
this.crypted_record = parsed;
|
||
}
|
||
},
|
||
async try_unlock() {
|
||
const {
|
||
user,
|
||
keypair,
|
||
errors
|
||
} = await decryptIdentityRecord(this.crypted_record, this.username, this.password);
|
||
if (user) {
|
||
this.username = user;
|
||
}
|
||
if (errors) {
|
||
this.errors.password = errors;
|
||
}
|
||
if (keypair) {
|
||
this.keypair = keypair;
|
||
}
|
||
},
|
||
async pasteClipboard() {
|
||
try {
|
||
this.raw_record = await navigator.clipboard.readText();
|
||
this.try_parsing()
|
||
} catch (e) {
|
||
console.error('Error:', e);
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
try {
|
||
fetch('/local/domains')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
this.domains = data;
|
||
});
|
||
} catch (e) {
|
||
console.error('Error:', e);
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.is-invalid input, .is-invalid select {
|
||
border: 1px solid var(--bs-danger);
|
||
}
|
||
|
||
.is-invalid .invalid-feedback {
|
||
display: block;
|
||
}
|
||
|
||
.input-group-text svg {
|
||
overflow: visible;
|
||
}
|
||
</style> |