- added backup codes for 2 factor authentication
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s
- npm updates - coverage validation: elevation ust be positive, depth must be negative - vinejs-provider.js: get enabled extensions from database, not via validOptions.extnames - vue components for backup codes: e.g.: PersonalSettings.vue - validate spaital coverage in leaflet map: draw.component.vue, map.component.vue - add backup code authentication into Login.vue - preset to use no preferred reviewer: Release.vue - 2 new vinejs validation rules: file_scan.ts and file-length.ts
This commit is contained in:
parent
ac473b1e72
commit
005df2e454
32 changed files with 1416 additions and 526 deletions
|
@ -4,13 +4,10 @@
|
|||
<fa-icon [icon]="faSearchLocation"></fa-icon>
|
||||
</button> -->
|
||||
<!-- -->
|
||||
<button
|
||||
ref="inputDraw"
|
||||
<button ref="inputDraw"
|
||||
class="inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 text-black border-teal-50 hover:bg-gray-200 text-sm p-1"
|
||||
type="button"
|
||||
:class="[_enabled ? 'cursor-not-allowed bg-cyan-200' : 'bg-teal-50 is-active']"
|
||||
@click.prevent="toggleDraw"
|
||||
>
|
||||
type="button" :class="[_enabled ? 'cursor-not-allowed bg-cyan-200' : 'bg-teal-50 is-active']"
|
||||
@click.prevent="toggleDraw">
|
||||
<BaseIcon v-if="mdiDrawPen" :path="mdiDrawPen" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -28,6 +25,8 @@ import { Map } from 'leaflet/src/map/index';
|
|||
import { on, off, preventDefault } from 'leaflet/src/dom/DomEvent';
|
||||
import { Rectangle } from 'leaflet/src/layer/vector/Rectangle';
|
||||
import { LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { LeafletMouseEvent } from 'leaflet';
|
||||
|
||||
@Component({
|
||||
name: 'draw-control',
|
||||
|
@ -58,19 +57,19 @@ export default class DrawControlComponent extends Vue {
|
|||
|
||||
@Prop() public mapId: string;
|
||||
// @Prop() public map: Map;
|
||||
@Prop public southWest: LatLngBounds;
|
||||
@Prop public northEast: LatLngBounds;
|
||||
@Prop public southWest: LatLng;
|
||||
@Prop public northEast: LatLng;
|
||||
@Prop({
|
||||
default: true,
|
||||
})
|
||||
public preserve: boolean;
|
||||
|
||||
mapService = MapService();
|
||||
public _enabled;
|
||||
public _enabled: boolean;
|
||||
private _map: Map;
|
||||
private _isDrawing: boolean = false;
|
||||
private _startLatLng;
|
||||
private _mapDraggable;
|
||||
private _startLatLng: LatLng;
|
||||
private _mapDraggable: boolean;
|
||||
private _shape: Rectangle | undefined;
|
||||
|
||||
enable() {
|
||||
|
@ -80,6 +79,7 @@ export default class DrawControlComponent extends Vue {
|
|||
|
||||
this._enabled = true;
|
||||
this.addHooks();
|
||||
this._map.control = this;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -111,6 +111,7 @@ export default class DrawControlComponent extends Vue {
|
|||
// this._map.domElement.style.cursor = 'crosshair';
|
||||
this._map._container.style.cursor = 'crosshair';
|
||||
// this._tooltip.updateContent({text: this._initialLabelText});
|
||||
|
||||
this._map
|
||||
.on('mousedown', this._onMouseDown, this)
|
||||
.on('mousemove', this._onMouseMove, this)
|
||||
|
@ -157,7 +158,7 @@ export default class DrawControlComponent extends Vue {
|
|||
this._isDrawing = false;
|
||||
}
|
||||
|
||||
private _onMouseDown(e) {
|
||||
private _onMouseDown(e: LeafletMouseEvent) {
|
||||
this._isDrawing = true;
|
||||
this._startLatLng = e.latlng;
|
||||
|
||||
|
@ -169,7 +170,7 @@ export default class DrawControlComponent extends Vue {
|
|||
preventDefault(e.originalEvent);
|
||||
}
|
||||
|
||||
private _onMouseMove(e) {
|
||||
private _onMouseMove(e: LeafletMouseEvent) {
|
||||
var latlng = e.latlng;
|
||||
|
||||
// this._tooltip.updatePosition(latlng);
|
||||
|
@ -191,13 +192,21 @@ export default class DrawControlComponent extends Vue {
|
|||
}
|
||||
}
|
||||
|
||||
private _fireCreatedEvent(shape) {
|
||||
private _fireCreatedEvent(shape: Rectangle) {
|
||||
var rectangle = new Rectangle(shape.getBounds(), this.options.shapeOptions);
|
||||
// L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle);
|
||||
this._map.fire('Draw.Event.CREATED', { layer: rectangle, type: this.TYPE });
|
||||
}
|
||||
|
||||
public drawShape(southWest, northEast) {
|
||||
public removeShape() {
|
||||
if (this._shape) {
|
||||
this._map.removeLayer(this._shape);
|
||||
// delete this._shape;
|
||||
this._shape = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public drawShape(southWest: LatLng, northEast: LatLng) {
|
||||
if (!this._shape) {
|
||||
const bounds = new LatLngBounds(southWest, northEast);
|
||||
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
||||
|
@ -210,7 +219,7 @@ export default class DrawControlComponent extends Vue {
|
|||
}
|
||||
|
||||
// from Draw Rectangle
|
||||
private _drawShape(latlng) {
|
||||
private _drawShape(latlng: LatLng) {
|
||||
if (!this._shape) {
|
||||
const bounds = new LatLngBounds(this._startLatLng, latlng);
|
||||
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
||||
|
|
|
@ -7,22 +7,32 @@
|
|||
<DrawControlComponent ref="draw" :mapId="mapId" :southWest="southWest" :northEast="northEast" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="gba-control-validate btn-group-vertical">
|
||||
<button
|
||||
class="min-w-27 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 text-black text-sm p-1"
|
||||
type="button"
|
||||
@click.stop.prevent="validateBoundingBox"
|
||||
:class="[validBoundingBox ? 'cursor-not-allowed bg-green-500 is-active' : 'bg-red-500 ']"
|
||||
>
|
||||
<!-- <BaseIcon v-if="mdiMapCheckOutline" :path="mdiMapCheckOutline" /> -->
|
||||
{{ label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { Component, Vue, Prop, Ref } from 'vue-facing-decorator';
|
||||
// import type { Coverage } from '@/Dataset';
|
||||
// import { Map, Control, MapOptions, LatLngBoundsExpression, tileLayer, latLng, latLngBounds, FeatureGroup } from 'leaflet';
|
||||
import { Map } from 'leaflet/src/map/index';
|
||||
import { Control } from 'leaflet/src/control/Control';
|
||||
import { LatLngBoundsExpression, toLatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
||||
import { toLatLng } from 'leaflet/src/geo/LatLng';
|
||||
import { LatLngBoundsExpression, LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
||||
// import { toLatLng } from 'leaflet/src/geo/LatLng';
|
||||
import { LatLng } from 'leaflet'; //'leaflet/src/geo/LatLng';
|
||||
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
|
||||
import { Attribution } from 'leaflet/src/control/Control.Attribution';
|
||||
// import { Attribution } from 'leaflet';
|
||||
// import { FeatureGroup } from 'leaflet/src/layer/FeatureGroup';
|
||||
import { mdiMapCheckOutline } from '@mdi/js';
|
||||
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||
|
||||
import { MapOptions } from './MapOptions';
|
||||
import { LayerOptions, LayerMap } from './LayerOptions';
|
||||
|
@ -32,6 +42,7 @@ import DrawControlComponent from './draw.component.vue';
|
|||
import { Coverage } from '@/Dataset';
|
||||
import { canvas } from 'leaflet/src/layer/vector/Canvas';
|
||||
import { svg } from 'leaflet/src/layer/vector/SVG';
|
||||
import Notification from '@/utils/toast';
|
||||
|
||||
Map.include({
|
||||
// @namespace Map; @method getRenderer(layer: Path): Renderer
|
||||
|
@ -84,6 +95,7 @@ const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://o
|
|||
components: {
|
||||
ZoomControlComponent,
|
||||
DrawControlComponent,
|
||||
BaseIcon,
|
||||
},
|
||||
})
|
||||
export default class MapComponent extends Vue {
|
||||
|
@ -119,14 +131,40 @@ export default class MapComponent extends Vue {
|
|||
@Prop()
|
||||
public baseMaps: LayerMap;
|
||||
|
||||
get label(): string {
|
||||
return this.validBoundingBox ? ' valid' : 'invalid';
|
||||
}
|
||||
|
||||
get validBoundingBox(): boolean {
|
||||
let isValidNumber =
|
||||
(typeof this.coverage.x_min === 'number' || !isNaN(Number(this.coverage.x_min))) &&
|
||||
(typeof this.coverage.y_min === 'number' || !isNaN(Number(this.coverage.y_min))) &&
|
||||
(typeof this.coverage.x_max === 'number' || !isNaN(Number(this.coverage.x_max))) &&
|
||||
(typeof this.coverage.y_max === 'number' || !isNaN(Number(this.coverage.y_max)));
|
||||
|
||||
let isBoundValid = true;
|
||||
if (isValidNumber) {
|
||||
let _southWest: LatLng = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
let _northEast: LatLng = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
||||
// this.draw.removeShape();
|
||||
// Notification.showTemporary('Bounds are not valid.');
|
||||
isBoundValid = false;
|
||||
}
|
||||
}
|
||||
return isValidNumber && isBoundValid;
|
||||
}
|
||||
|
||||
@Ref('zoom') private zoom: ZoomControlComponent;
|
||||
@Ref('draw') private draw: DrawControlComponent;
|
||||
|
||||
// services:
|
||||
mapService = MapService();
|
||||
|
||||
southWest;
|
||||
northEast;
|
||||
mdiMapCheckOutline = mdiMapCheckOutline;
|
||||
southWest: LatLng;
|
||||
northEast: LatLng;
|
||||
|
||||
/**
|
||||
* Informs when initialization is done with map id.
|
||||
|
@ -136,8 +174,65 @@ export default class MapComponent extends Vue {
|
|||
public map!: Map;
|
||||
// protected drawnItems!: FeatureGroup<any>;
|
||||
|
||||
// @Prop({ type: Object })
|
||||
// geolocation: Coverage;
|
||||
validateBoundingBox() {
|
||||
if (this.validBoundingBox == false) {
|
||||
this.draw.removeShape();
|
||||
Notification.showError('Bounds are not valid.');
|
||||
return;
|
||||
}
|
||||
this.map.control && this.map.control.disable();
|
||||
var _this = this;
|
||||
// // _this.locationErrors.length = 0;
|
||||
// this.drawnItems.clearLayers();
|
||||
// //var xmin = document.getElementById("xmin").value;
|
||||
// var xmin = (<HTMLInputElement>document.getElementById("xmin")).value;
|
||||
// // var ymin = document.getElementById("ymin").value;
|
||||
// var ymin = (<HTMLInputElement>document.getElementById("ymin")).value;
|
||||
// //var xmax = document.getElementById("xmax").value;
|
||||
// var xmax = (<HTMLInputElement>document.getElementById("xmax")).value;
|
||||
// //var ymax = document.getElementById("ymax").value;
|
||||
// var ymax = (<HTMLInputElement>document.getElementById("ymax")).value;
|
||||
// var bounds = [[ymin, xmin], [ymax, xmax]];
|
||||
|
||||
// let _southWest: LatLng;
|
||||
// let _northEast: LatLng;
|
||||
// if (this.coverage.x_min && this.coverage.y_min) {
|
||||
let _southWest: LatLng = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
// }
|
||||
// if (this.coverage.x_max && this.coverage.y_max) {
|
||||
let _northEast: LatLng = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
// }
|
||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
||||
this.draw.removeShape();
|
||||
Notification.showTemporary('Bounds are not valid.');
|
||||
} else {
|
||||
// this.draw.drawShape(_southWest, _northEast);
|
||||
try {
|
||||
this.draw.drawShape(_southWest, _northEast);
|
||||
_this.map.fitBounds(bounds);
|
||||
|
||||
// var boundingBox = L.rectangle(bounds, { color: "#005F6A", weight: 1 });
|
||||
// // this.geolocation.xmin = xmin;
|
||||
// // this.geolocation.ymin = ymin;
|
||||
// // this.geolocation.xmax = xmax;
|
||||
// // this.geolocation.ymax = ymax;
|
||||
|
||||
// _this.drawnItems.addLayer(boundingBox);
|
||||
// _this.map.fitBounds(bounds);
|
||||
// this.options.message = "valid bounding box";
|
||||
// this.$toast.success("valid bounding box", this.options);
|
||||
Notification.showSuccess('valid bounding box');
|
||||
} catch (err) {
|
||||
// this.options.message = e.message;
|
||||
// // _this.errors.push(e);
|
||||
// this.$toast.error(e.message, this.options);
|
||||
Notification.showTemporary('An error occurred while drawing bounding box');
|
||||
// generatingCodes.value = false;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mounted(): void {
|
||||
this.initMap();
|
||||
|
@ -195,26 +290,26 @@ export default class MapComponent extends Vue {
|
|||
// this.map.fitBounds(this.fitBounds);
|
||||
// }
|
||||
if (this.coverage.x_min && this.coverage.y_min) {
|
||||
this.southWest = toLatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
this.southWest = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
} else {
|
||||
this.southWest = toLatLng(46.5, 9.9);
|
||||
this.southWest = new LatLng(46.5, 9.9);
|
||||
}
|
||||
if (this.coverage.x_max && this.coverage.y_max) {
|
||||
this.northEast = toLatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
this.northEast = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
} else {
|
||||
this.northEast = toLatLng(48.9, 16.9);
|
||||
this.northEast = new LatLng(48.9, 16.9);
|
||||
} // this.northEast = toLatLng(48.9, 16.9);
|
||||
const bounds = toLatLngBounds(this.southWest, this.northEast);
|
||||
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||
map.fitBounds(bounds);
|
||||
|
||||
if (this.coverage.x_min && this.coverage.x_max && this.coverage.y_min && this.coverage.y_max) {
|
||||
let _southWest;
|
||||
let _northEast;
|
||||
let _southWest: LatLng;
|
||||
let _northEast: LatLng;
|
||||
if (this.coverage.x_min && this.coverage.y_min) {
|
||||
_southWest = toLatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
_southWest = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||
}
|
||||
if (this.coverage.x_max && this.coverage.y_max) {
|
||||
_northEast = toLatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
_northEast = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||
}
|
||||
this.draw.drawShape(_southWest, _northEast);
|
||||
}
|
||||
|
@ -257,6 +352,26 @@ export default class MapComponent extends Vue {
|
|||
background: none;
|
||||
}
|
||||
|
||||
.gba-control-validate {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 150px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.btn-group-vertical button {
|
||||
display: block;
|
||||
|
||||
margin-left: 0;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
/* .leaflet-pane {
|
||||
z-index: 30;
|
||||
} */
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
ref="inputPlus"
|
||||
class="disabled:bg-gray-200 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 bg-teal-50 text-black border-teal-50 text-sm p-1"
|
||||
type="button"
|
||||
@click.prevent="zoomIn"
|
||||
@click.stop.prevent="zoomIn"
|
||||
>
|
||||
<BaseIcon v-if="mdiPlus" :path="mdiPlus" />
|
||||
</button>
|
||||
|
@ -13,7 +13,7 @@
|
|||
ref="inputMinus"
|
||||
class="disabled:bg-gray-200 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 bg-teal-50 text-black border-teal-50 text-sm p-1"
|
||||
type="button"
|
||||
@click.prevent="zoomOut"
|
||||
@click.stop.prevent="zoomOut"
|
||||
>
|
||||
<BaseIcon v-if="mdiMinus" :path="mdiMinus" />
|
||||
</button>
|
||||
|
|
152
resources/js/Components/PersonalSettings.vue
Normal file
152
resources/js/Components/PersonalSettings.vue
Normal file
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<div>
|
||||
<BaseButton v-if="!enabled" id="generate-backup-codes" class="mx-2" :icon="mdiContentSaveCheck" type="button"
|
||||
color="info" :class="{ 'icon-loading-small': generatingCodes }" :disabled="generatingCodes"
|
||||
label=" Generate backup codes" @click="generateBackupCodes" />
|
||||
|
||||
<template v-else>
|
||||
|
||||
<template v-if="!haveCodes">
|
||||
{{ `Backup codes have been generated. ${used} of ${total} codes have been used.` }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
These are your backup codes. Please save and/or print them as you will not be able to read the codes
|
||||
again later
|
||||
<ul>
|
||||
<li v-for="code in codes" :key="code" class="backup-code">
|
||||
{{ code }}
|
||||
</li>
|
||||
</ul>
|
||||
<BaseButton :href="downloadUrl" class="mt-2 mb-2" :download="downloadFilename" rounded-full small
|
||||
:icon="mdiContentSave" :label="'Save backup codes'">
|
||||
</BaseButton>
|
||||
<BaseButton @click="printCodes" rounded-full small :icon="mdiContentSave"
|
||||
:label="'Print backup codes'">
|
||||
</BaseButton>
|
||||
<!-- <button class="button" @click="printCodes">
|
||||
{{ t('twofactor_backupcodes', 'Print backup codes') }}
|
||||
</button> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||
<BaseButton class="mt-2 mb-2" :icon="mdiContentSaveCheck" type="button" color="info"
|
||||
:disabled="generatingCodes" label="Regenerate backup codes" @click="generateBackupCodes" />
|
||||
</div>
|
||||
<p>
|
||||
<em> 'twofactor_backupcodes', `If you regenerate backup codes, you automatically invalidate old codes.`
|
||||
</em>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, Ref } from 'vue';
|
||||
import { ComputedRef } from 'vue';
|
||||
import { MainService } from '@/Stores/main';
|
||||
import Notification from '@/utils/toast';
|
||||
import { mdiContentSaveCheck, mdiContentSave } from '@mdi/js';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
// import { useI18n } from 'vue-i18n';
|
||||
// import { confirmPassword } from '@nextcloud/password-confirmation'
|
||||
// import '@nextcloud/password-confirmation/dist/style.css'
|
||||
// import { print } from '../service/PrintService.js'
|
||||
// const { t } = useI18n();
|
||||
|
||||
const generatingCodes: Ref<boolean> = ref(false);
|
||||
const mainService = MainService();
|
||||
|
||||
const props = defineProps({
|
||||
backupState: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
if (props.backupState.enabled) {
|
||||
mainService.backupcodesEnabled = true;
|
||||
mainService.total = props.backupState.total;
|
||||
mainService.used = props.backupState.used;
|
||||
}
|
||||
|
||||
const enabled = computed(() => mainService.backupcodesEnabled);
|
||||
const total = computed(() => mainService.total);
|
||||
const used = computed(() => mainService.used);
|
||||
const codes: ComputedRef<string[]> = computed(() => mainService.codes);
|
||||
const haveCodes = computed(() => {
|
||||
return codes && codes.value.length > 0;
|
||||
});
|
||||
const downloadFilename = computed(() => {
|
||||
return 'tethys-backup-codes.txt';
|
||||
});
|
||||
const downloadUrl = computed(() => {
|
||||
if (!codes) {
|
||||
return '';
|
||||
}
|
||||
return (
|
||||
'data:text/plain,' +
|
||||
encodeURIComponent(
|
||||
codes.value.reduce((prev, code) => {
|
||||
return prev + code + '\r\n';
|
||||
}, ''),
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const print = (data: any) => {
|
||||
const name = 'Tethys';
|
||||
// const newTab = window.open('', `${name} backup codes`)
|
||||
const newTab = window.open('about:blank', `${name} backup codes`);
|
||||
if (newTab) {
|
||||
newTab.document.write('<h1>' + `${name} backup codes` + '</h1>');
|
||||
newTab.document.write('<pre>' + data + '</pre>');
|
||||
newTab.print();
|
||||
newTab.close();
|
||||
}
|
||||
};
|
||||
const getPrintData = (codes: string[]) => {
|
||||
if (!codes) {
|
||||
return '';
|
||||
}
|
||||
return codes.reduce((prev, code) => {
|
||||
return prev + code + '<br>';
|
||||
}, '');
|
||||
};
|
||||
|
||||
const printCodes = () => {
|
||||
const data = getPrintData(codes.value);
|
||||
print(data);
|
||||
};
|
||||
|
||||
const generateBackupCodes = async () => {
|
||||
// Hide old codes
|
||||
generatingCodes.value = true;
|
||||
|
||||
try {
|
||||
await mainService.generate();
|
||||
generatingCodes.value = false;
|
||||
} catch (err) {
|
||||
Notification.showTemporary('An error occurred while generating your backup codes');
|
||||
generatingCodes.value = false;
|
||||
throw err;
|
||||
}
|
||||
|
||||
// this.$store.dispatch('generate').then(data => {
|
||||
// this.generatingCodes = false
|
||||
// }).catch(err => {
|
||||
// OC.Notification.showTemporary(t('twofactor_backupcodes', 'An error occurred while generating your backup codes'))
|
||||
// this.generatingCodes = false
|
||||
// throw err
|
||||
// })
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.backup-code {
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
</style>
|
|
@ -44,6 +44,8 @@
|
|||
:loading="loadingConfirmation" v-model:confirmation="confirmationCode" @confirm="enableTOTP" />
|
||||
|
||||
|
||||
<BaseDivider></BaseDivider>
|
||||
<PersonalSettings :backupState="props.backupState"/>
|
||||
|
||||
</CardBox>
|
||||
</template>
|
||||
|
@ -56,6 +58,8 @@ import SetupConfirmation from '@/Components/SetupConfirmation.vue';
|
|||
|
||||
import Notification from '@/utils/toast';
|
||||
import { mdiTwoFactorAuthentication } from '@mdi/js';
|
||||
import PersonalSettings from '@/Components/PersonalSettings.vue';
|
||||
import BaseDivider from './BaseDivider.vue';
|
||||
|
||||
const mainService = MainService();
|
||||
// const emit = defineEmits(['confirm', 'update:confirmation']);
|
||||
|
@ -70,6 +74,10 @@ const props = defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
backupState: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// // code: {
|
||||
// // type: Object,
|
||||
// // },
|
||||
|
|
|
@ -29,6 +29,7 @@ import { computed, Ref } from 'vue';
|
|||
import { usePage } from '@inertiajs/vue3';
|
||||
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
|
||||
import PersonalTotpSettings from '@/Components/PersonalTotpSettings.vue';
|
||||
// import PersonalSettings from '@/Components/PersonalSettings.vue';
|
||||
// import { MainService } from '@/Stores/main';
|
||||
// const mainService = MainService();
|
||||
|
||||
|
@ -47,9 +48,9 @@ defineProps({
|
|||
code: {
|
||||
type: Object,
|
||||
},
|
||||
recoveryCodes: {
|
||||
type: Array<string>,
|
||||
default: () => [],
|
||||
backupState: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
|
@ -211,7 +212,10 @@ const flash: Ref<any> = computed(() => {
|
|||
|
||||
|
||||
|
||||
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled"/>
|
||||
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled" :backupState="backupState">
|
||||
</PersonalTotpSettings>
|
||||
<!-- <PersonalSettings :state="backupState"/> -->
|
||||
|
||||
<!-- <CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
|
||||
@submit.prevent="enableTwoFactorAuthentication()">
|
||||
<div class="text-lg font-medium text-gray-900">
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
|
||||
|
@ -11,8 +10,7 @@
|
|||
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
|
||||
</a>
|
||||
|
||||
<CardBox v-if="isTwoFactorAuthNeeded == false" :class="cardClass" form
|
||||
@submit.prevent="submit">
|
||||
<CardBox v-if="isTwoFactorAuthNeeded == false" :class="cardClass" form @submit.prevent="submit">
|
||||
<FormValidationErrors v-bind:errors="errors" />
|
||||
|
||||
<NotificationBarInCard v-if="status" color="info">
|
||||
|
@ -47,8 +45,8 @@
|
|||
<!-- buttons -->
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label=" Login to your account" :class="{ 'opacity-25': form.processing }"
|
||||
v-bind:disabled="form.processing" />
|
||||
<BaseButton type="submit" color="info" label=" Login to your account"
|
||||
:class="{ 'opacity-25': form.processing }" v-bind:disabled="form.processing" />
|
||||
<!-- <button type="submit" v-bind:disabled="form.processing" :class="{ 'opacity-25': form.processing }"
|
||||
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">
|
||||
Login to your account
|
||||
|
@ -66,10 +64,29 @@
|
|||
|
||||
<CardBox v-else-if="isTwoFactorAuthNeeded" :icon="mdiTwoFactorAuthentication" :class="cardClass" form
|
||||
@submit.prevent="submitFa2Form">
|
||||
<FormField label="2FA Code" label-for="code" help="Please enter 2factor code">
|
||||
|
||||
<!-- authentication method menu -->
|
||||
<div class="lex flex-col md:flex-row mb-3">
|
||||
<label for="authmethod-option-one" class="pure-radio">
|
||||
<input id="authmethod-option-one" type="radio" v-model="authMethod" value="code" />
|
||||
use 2fa app
|
||||
</label>
|
||||
<label for="authmethod-option-two" class="pure-radio">
|
||||
<input id="authmethod-option-two" type="radio" v-model="authMethod" value="backupCode" />
|
||||
use backup code
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<FormField v-if="authMethod === 'code'" label="2FA Code" label-for="code" help="Please enter 2factor code">
|
||||
<FormControl v-model="fa2Form.code" :icon="mdiAccount" id="code" type="tel" required />
|
||||
</FormField>
|
||||
|
||||
<FormField v-if="authMethod === 'backupCode'" label="Backup Code" label-for="backupCode" help="Please enter backup code">
|
||||
<FormControl v-model="fa2Form.backup_code" :icon="mdiAccount" id="backupCode" type="tel" required />
|
||||
</FormField>
|
||||
|
||||
|
||||
<div v-if="flash && flash.message" class="flex flex-col mt-6 animate-fade-in">
|
||||
<div class="bg-yellow-500 border-l-4 border-orange-400 text-white p-4" role="alert">
|
||||
<p class="font-bold">Be Warned</p>
|
||||
|
@ -85,8 +102,8 @@
|
|||
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">
|
||||
Verify
|
||||
</button> -->
|
||||
<BaseButton type="submit" :icon="mdiContentSaveCheck" color="info" label=" Login to your account" :class="{ 'opacity-25': fa2Form.processing }"
|
||||
v-bind:disabled="fa2Form.processing" />
|
||||
<BaseButton type="submit" :icon="mdiContentSaveCheck" color="info" label=" Login to your account"
|
||||
:class="{ 'opacity-25': fa2Form.processing }" v-bind:disabled="fa2Form.processing" />
|
||||
</BaseButtons>
|
||||
<!-- <Link :href="stardust.route('app.register.show')"> Register </Link> -->
|
||||
|
||||
|
@ -136,6 +153,15 @@ const user_id: Ref<any> = computed(() => {
|
|||
|
||||
const isTwoFactorAuthNeeded = ref(false);
|
||||
|
||||
const authMethod = ref('code');
|
||||
watch(authMethod, (currentValue) => {
|
||||
if (currentValue == 'code') {
|
||||
fa2Form.backup_code = undefined;
|
||||
} else if (currentValue == 'backupCode') {
|
||||
fa2Form.code = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// const user_id: ComputedRef<number> = computed(() => {
|
||||
// return usePage().props.flash.user_id as number;
|
||||
// });
|
||||
|
@ -230,7 +256,7 @@ const submit = async () => {
|
|||
remember: form.remember && form.remember.length ? 'on' : '',
|
||||
}))
|
||||
.post('/app/login', {
|
||||
// .post(stardust.route('login.store'), {
|
||||
// .post(stardust.route('login.store'), {
|
||||
onFinish: () => {
|
||||
form.reset('password');
|
||||
},
|
||||
|
@ -240,14 +266,15 @@ const submit = async () => {
|
|||
|
||||
const fa2Form: InertiaForm = useForm(() => ({
|
||||
code: '',
|
||||
remember: [],
|
||||
backup_code: '',
|
||||
// remember: [],
|
||||
login_id: ''
|
||||
}));
|
||||
const submitFa2Form = async () => {
|
||||
await fa2Form
|
||||
.transform((data: InertiaForm) => ({
|
||||
...data,
|
||||
remember: fa2Form.remember && fa2Form.remember.length ? 'on' : '',
|
||||
// remember: fa2Form.remember && fa2Form.remember.length ? 'on' : '',
|
||||
login_id: user_id.value
|
||||
}))
|
||||
.post(stardust.route('login.twoFactorChallenge'), {
|
||||
|
|
|
@ -306,8 +306,6 @@ const prevStep = () => {
|
|||
const submit = async () => {
|
||||
let route = stardust.route('dataset.submit');
|
||||
|
||||
// this.currentStatus = STATUS_SAVING;
|
||||
// serrors = [];
|
||||
const files = form.files.map((obj) => {
|
||||
return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
|
||||
});
|
||||
|
@ -808,7 +806,7 @@ Removes a selected keyword
|
|||
<FormField label="Coverage X Min"
|
||||
:class="{ 'text-red-400': form.errors['coverage.x_min'] }"
|
||||
class="w-full mx-2 flex-1">
|
||||
<FormControl required v-model="form.coverage.x_min" type="text"
|
||||
<FormControl required v-model="form.coverage.x_min" type="text" inputmode="numeric" pattern="\d*"
|
||||
placeholder="[enter x_min]">
|
||||
<div class="text-red-400 text-sm"
|
||||
v-if="form.errors['coverage.x_min'] && Array.isArray(form.errors['coverage.x_min'])">
|
||||
|
|
|
@ -31,7 +31,7 @@ const errors: Ref<any> = computed(() => {
|
|||
const form = useForm({
|
||||
preferred_reviewer: '',
|
||||
preferred_reviewer_email: '',
|
||||
preferation: 'yes_preferation',
|
||||
preferation: 'no_preferation',
|
||||
|
||||
// preferation: '',
|
||||
// isPreferationRequired: false,
|
||||
|
|
|
@ -35,6 +35,14 @@ export const saveState = async (data) => {
|
|||
return resp.data;
|
||||
};
|
||||
|
||||
export const generateCodes = async () => {
|
||||
const url = '/api/twofactor_backupcodes/settings/create';
|
||||
|
||||
// return Axios.post(url, {}).then((resp) => resp.data);
|
||||
const resp = await axios.post(url, {});
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
// Anfrage staet : 1
|
||||
|
||||
// ANtwort json:
|
||||
|
@ -68,11 +76,16 @@ export const MainService = defineStore('main', {
|
|||
|
||||
totpState: 0,
|
||||
|
||||
backupcodesEnabled: false,
|
||||
total: 0,
|
||||
used: 0,
|
||||
codes: [],
|
||||
|
||||
graphData: {},
|
||||
}),
|
||||
actions: {
|
||||
// payload = authenticated user
|
||||
setUser(payload) {
|
||||
setUser(payload: any) {
|
||||
if (payload.name) {
|
||||
this.userName = payload.name;
|
||||
}
|
||||
|
@ -84,7 +97,7 @@ export const MainService = defineStore('main', {
|
|||
}
|
||||
},
|
||||
|
||||
setDataset(payload) {
|
||||
setDataset(payload: any) {
|
||||
this.dataset = payload;
|
||||
|
||||
// this.dataset = {
|
||||
|
@ -109,7 +122,7 @@ export const MainService = defineStore('main', {
|
|||
this.dataset = null;
|
||||
},
|
||||
|
||||
fetch(sampleDataKey) {
|
||||
fetch(sampleDataKey: any) {
|
||||
// sampleDataKey= clients or history
|
||||
axios
|
||||
.get(`/data-sources/${sampleDataKey}.json`)
|
||||
|
@ -155,8 +168,12 @@ export const MainService = defineStore('main', {
|
|||
},
|
||||
|
||||
async disable() {
|
||||
const { state } = await saveState({ state: State.STATE_DISABLED });
|
||||
const { state, backupState } = await saveState({ state: State.STATE_DISABLED });
|
||||
this.totpState = state;
|
||||
|
||||
this.backupcodesEnabled = backupState.enabled;
|
||||
this.total = backupState.total;
|
||||
this.used = backupState.used;
|
||||
},
|
||||
|
||||
async confirm(code: string) {
|
||||
|
@ -181,6 +198,15 @@ export const MainService = defineStore('main', {
|
|||
});
|
||||
},
|
||||
|
||||
async generate(): Promise<void> {
|
||||
const { codes, backupState } = await generateCodes();
|
||||
// this.totpState = state;
|
||||
this.codes = codes;
|
||||
this.backupcodesEnabled = backupState.enabled;
|
||||
this.total = backupState.total;
|
||||
this.used = backupState.used;
|
||||
},
|
||||
|
||||
// fetchfiles(id) {
|
||||
// // sampleDataKey= authors or datasets
|
||||
// axios
|
||||
|
|
|
@ -11,6 +11,7 @@ import { darkModeKey, styleKey } from '@/config';
|
|||
// import type { DefineComponent } from 'vue';
|
||||
// import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
||||
const pinia = createPinia();
|
||||
import i18n from './i18n';
|
||||
import { EmitterPlugin } from '@/EmitterDirective';
|
||||
|
||||
import { initRoutes } from '@eidellev/adonis-stardust/client/index.js';
|
||||
|
@ -62,6 +63,7 @@ createInertiaApp({
|
|||
createApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.use(pinia)
|
||||
.use(i18n)
|
||||
.use(EmitterPlugin)
|
||||
// .component('inertia-link', Link)
|
||||
.mount(el);
|
||||
|
|
17
resources/js/i18n/index.ts
Normal file
17
resources/js/i18n/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { createI18n } from 'vue-i18n';
|
||||
const i18n = createI18n({
|
||||
// default locale
|
||||
locale: 'en',
|
||||
// translations
|
||||
messages: {
|
||||
en: {
|
||||
appTitle: 'Mushahed',
|
||||
twofactor_backupcodes: '',
|
||||
},
|
||||
de: {
|
||||
appTitle: 'مشاهد',
|
||||
twofactor_backupcodes: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
export default i18n;
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue