- added backup codes for 2 factor authentication
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:
Kaimbacher 2024-07-08 13:52:20 +02:00
parent ac473b1e72
commit 005df2e454
32 changed files with 1416 additions and 526 deletions

View file

@ -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);

View file

@ -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 = '&copy; <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;
} */

View file

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

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

View file

@ -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,
// // },