web/: add custom linter for requiring @vue/component

Also ignore coverage folder for linting. I had to add a new
.stylelintignore file, because ignoreFiles property was not properly
working.

Change-Id: Iadd99b64eadd9c4103f750519263113ae8780ce1
This commit is contained in:
Egon Elbre 2021-08-31 18:25:49 +03:00
parent ee4361fe0d
commit e5977ec849
19 changed files with 852 additions and 711 deletions

46
web/eslint-storj/index.js Normal file
View File

@ -0,0 +1,46 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
module.exports = {
rules: {
"vue/require-annotation": {
meta: {
fixable: "code",
},
create: function(context) {
return {
Decorator(node) {
let isComponent = false;
const expr = node.expression;
if(expr.name === "Component"){
isComponent = true;
} else if (expr.callee && expr.callee.name === "Component"){
isComponent = true;
}
if(!isComponent){ return; }
const commentsBefore = context.getCommentsBefore(node);
const decoratorLine = node.loc.start.line;
let annotated = false;
commentsBefore.forEach(comment => {
if(comment.loc.start.line === decoratorLine - 1){
if(comment.value.trim() === "@vue/component") {
annotated = true;
}
}
})
if(!annotated){
context.report({
node: node,
message: '@Component requires // @vue/component',
fix: function(fixer) {
return fixer.insertTextBefore(node, "// @vue/component\n");
}
});
}
}
};
}
}
}
};

View File

@ -0,0 +1,5 @@
{
"name": "eslint-plugin-storj",
"version": "1.0.0",
"main": "index.js"
}

View File

@ -14,7 +14,10 @@ module.exports = {
parserOptions: {
ecmaVersion: 2020
},
plugins: ["storj"],
rules: {
"linebreak-style": ["error", "unix"],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
@ -41,5 +44,7 @@ module.exports = {
"vue/no-useless-v-bind": ["warn"],
'vue/no-unregistered-components': ['warn', { ignorePatterns: ['router-link', 'router-view'] }],
'storj/vue/require-annotation': 'warn',
},
}

View File

@ -0,0 +1,11 @@
*.*
!*.vue
!*.css
!*.sss
!*.less
!*.scss
!*.sass
dist
node_modules
coverage

View File

@ -11,7 +11,6 @@ module.exports = {
"stylelint-scss"
],
"extends": "stylelint-config-standard",
"ignoreFiles": ["dist/**"],
"rules": {
"indentation": 4,
"string-quotes": "single",

View File

@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
"name": "multinode",
"version": "0.0.1",
"dependencies": {
"chart.js": "2.9.4",
@ -33,6 +34,7 @@
"compression-webpack-plugin": "6.0.0",
"core-js": "3.6.5",
"eslint": "6.7.2",
"eslint-plugin-storj": "file:../eslint-storj",
"eslint-plugin-vue": "7.16.0",
"jest-fetch-mock": "3.0.0",
"sass": "1.37.0",
@ -49,6 +51,11 @@
"vue-template-compiler": "2.6.11"
}
},
"../eslint-storj": {
"name": "eslint-plugin-storj",
"version": "1.0.0",
"dev": true
},
"node_modules/@babel/code-frame": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
@ -10722,6 +10729,10 @@
"rimraf": "bin.js"
}
},
"node_modules/eslint-plugin-storj": {
"resolved": "../eslint-storj",
"link": true
},
"node_modules/eslint-plugin-vue": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.16.0.tgz",
@ -36217,6 +36228,9 @@
}
}
},
"eslint-plugin-storj": {
"version": "file:../eslint-storj"
},
"eslint-plugin-vue": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.16.0.tgz",

View File

@ -4,8 +4,8 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"lint": "vue-cli-service lint --max-warnings 0 --fix && stylelint --max-warnings 0 \"**/*.{vue,css,sss,less,scss,sass}\" --fix",
"lint-ci": "vue-cli-service lint --max-warnings 0 --no-fix && stylelint --max-warnings 0 --no-fix \"**/*.{vue,css,sss,less,scss,sass}\"",
"lint": "vue-cli-service lint --max-warnings 0 --fix && stylelint . --max-warnings 0 --fix",
"lint-ci": "vue-cli-service lint --max-warnings 0 --no-fix && stylelint . --max-warnings 0 --no-fix",
"build": "vue-cli-service build",
"dev": "vue-cli-service build --mode development --watch",
"test": "vue-cli-service test:unit"
@ -38,6 +38,7 @@
"core-js": "3.6.5",
"eslint": "6.7.2",
"eslint-plugin-vue": "7.16.0",
"eslint-plugin-storj": "file:../eslint-storj",
"jest-fetch-mock": "3.0.0",
"sass": "1.37.0",
"sass-loader": "8.0.0",

View File

@ -14,7 +14,10 @@ module.exports = {
parserOptions: {
ecmaVersion: 2020
},
plugins: ["storj"],
rules: {
"linebreak-style": ["error", "unix"],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
@ -41,5 +44,7 @@ module.exports = {
"vue/no-useless-v-bind": ["warn"],
'vue/no-unregistered-components': ['warn', { ignorePatterns: ['router-link', 'router-view'] }],
'storj/vue/require-annotation': 'warn',
},
}

View File

@ -0,0 +1,13 @@
*.*
!*.vue
!*.css
!*.sss
!*.less
!*.scss
!*.sass
dist
node_modules
coverage
Dockerfile
entrypoint

View File

@ -11,7 +11,6 @@ module.exports = {
"stylelint-scss"
],
"extends": "stylelint-config-standard",
"ignoreFiles": ["dist/**"],
"rules": {
"indentation": 4,
"string-quotes": "single",

View File

@ -52,6 +52,7 @@
"babel-eslint": "10.1.0",
"compression-webpack-plugin": "6.0.0",
"eslint": "6.7.2",
"eslint-plugin-storj": "file:../eslint-storj",
"eslint-plugin-vue": "7.16.0",
"jest-fetch-mock": "3.0.3",
"sass": "1.37.0",
@ -68,6 +69,11 @@
"worker-plugin": "5.0.0"
}
},
"../eslint-storj": {
"name": "eslint-plugin-storj",
"version": "1.0.0",
"dev": true
},
"node_modules/@babel/code-frame": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
@ -11802,6 +11808,10 @@
"rimraf": "bin.js"
}
},
"node_modules/eslint-plugin-storj": {
"resolved": "../eslint-storj",
"link": true
},
"node_modules/eslint-plugin-vue": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.16.0.tgz",
@ -39522,6 +39532,9 @@
}
}
},
"eslint-plugin-storj": {
"version": "file:../eslint-storj"
},
"eslint-plugin-vue": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.16.0.tgz",

View File

@ -4,8 +4,8 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"lint": "vue-cli-service lint --max-warnings 0 && stylelint --max-warnings 0 \"**/*.{vue,css,sss,less,scss,sass}\" --fix",
"lint-ci": "vue-cli-service lint --max-warnings 0 --no-fix && stylelint --max-warnings 0 --no-fix \"**/*.{vue,css,sss,less,scss,sass}\"",
"lint": "vue-cli-service lint --max-warnings 0 --fix && stylelint . --max-warnings 0 --fix",
"lint-ci": "vue-cli-service lint --max-warnings 0 --no-fix && stylelint . --max-warnings 0 --no-fix",
"build": "vue-cli-service build",
"wasm": "chmod +x ./scripts/build-wasm.sh && ./scripts/build-wasm.sh",
"dev": "vue-cli-service build --mode development --watch",
@ -57,6 +57,7 @@
"compression-webpack-plugin": "6.0.0",
"eslint": "6.7.2",
"eslint-plugin-vue": "7.16.0",
"eslint-plugin-storj": "file:../eslint-storj",
"jest-fetch-mock": "3.0.3",
"sass": "1.37.0",
"sass-loader": "10.0.2",

View File

@ -1,358 +1,358 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="forgot-area" @keyup.enter="onSendConfigurations">
<div class="forgot-area__logo-wrapper">
<LogoIcon class="forgot-area__logo-wrapper__logo" @click="onLogoClick" />
</div>
<div class="forgot-area__content-area">
<div class="forgot-area__content-area__container">
<div class="forgot-area__content-area__container__title-area">
<h1 class="forgot-area__content-area__container__title-area__title">Reset Password</h1>
<div class="forgot-area__expand" @click.stop="toggleDropdown">
<span class="forgot-area__expand__value">{{ satelliteName }}</span>
<BottomArrowIcon />
<div v-if="isDropdownShown" v-click-outside="closeDropdown" class="forgot-area__expand__dropdown">
<div class="forgot-area__expand__dropdown__item" @click.stop="closeDropdown">
<SelectedCheckIcon />
<span class="forgot-area__expand__dropdown__item__name">{{ satelliteName }}</span>
</div>
<a v-for="sat in partneredSatellites" :key="sat.id" class="forgot-area__expand__dropdown__item" :href="sat.address + '/forgot-password'">
{{ sat.name }}
</a>
</div>
</div>
</div>
<p class="forgot-area__content-area__container__message">If youve forgotten your account password, you can reset it here. Make sure youre signing in to the right satellite.</p>
<div class="forgot-area__content-area__container__input-wrapper">
<HeaderlessInput
class="full-input"
label="Email Address"
placeholder="example@email.com"
:error="emailError"
width="calc(100% - 2px)"
height="46px"
@setData="setEmail"
/>
</div>
<p class="forgot-area__content-area__container__button" @click.prevent="onSendConfigurations">Reset Password</p>
</div>
<div class="forgot-area__content-area__login-container">
<router-link :to="loginPath" class="forgot-area__content-area__login-container__link">
Back to Login
</router-link>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import BottomArrowIcon from '@/../static/images/common/lightBottomArrow.svg';
import SelectedCheckIcon from '@/../static/images/common/selectedCheck.svg';
import LogoIcon from '@/../static/images/logo.svg';
import { AuthHttpApi } from '@/api/auth';
import { RouteConfig } from '@/router';
import { PartneredSatellite } from '@/types/common';
import { Validator } from '@/utils/validation';
// @vue/component
@Component({
components: {
HeaderlessInput,
BottomArrowIcon,
SelectedCheckIcon,
LogoIcon,
},
})
export default class ForgotPassword extends Vue {
private email = '';
private emailError = '';
private readonly auth: AuthHttpApi = new AuthHttpApi();
// tardigrade logic
public isDropdownShown = false;
public readonly loginPath: string = RouteConfig.Login.path;
/**
* Sets the email field to the given value.
*/
public setEmail(value: string): void {
this.email = value;
this.emailError = '';
}
/**
* Name of the current satellite.
*/
public get satelliteName(): string {
return this.$store.state.appStateModule.satelliteName;
}
/**
* Information about partnered satellites, including name and signup link.
*/
public get partneredSatellites(): PartneredSatellite[] {
return this.$store.state.appStateModule.partneredSatellites;
}
/**
* Toggles satellite selection dropdown visibility (Tardigrade).
*/
public toggleDropdown(): void {
this.isDropdownShown = !this.isDropdownShown;
}
/**
* Closes satellite selection dropdown (Tardigrade).
*/
public closeDropdown(): void {
this.isDropdownShown = false;
}
/**
* Sends recovery password email.
*/
public async onSendConfigurations(): Promise<void> {
if (!this.validateFields()) {
return;
}
try {
await this.auth.forgotPassword(this.email);
} catch (error) {
await this.$notify.error(error.message);
return;
}
await this.$notify.success('Please look for instructions at your email');
}
/**
* Changes location to Login route.
*/
public onBackToLoginClick(): void {
this.$router.push(RouteConfig.Login.path);
}
/**
* Reloads the page.
*/
public onLogoClick(): void {
location.reload();
}
/**
* Returns whether the email address is properly structured.
*/
private validateFields(): boolean {
const isEmailValid = Validator.email(this.email.trim());
if (!isEmailValid) {
this.emailError = 'Invalid Email';
}
return isEmailValid;
}
}
</script>
<style scoped lang="scss">
.forgot-area {
display: flex;
flex-direction: column;
justify-content: flex-start;
font-family: 'font_regular', sans-serif;
background-color: #f5f6fa;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-height: 100%;
overflow-y: scroll;
&__logo-wrapper {
text-align: center;
margin: 70px 0;
&__logo {
cursor: pointer;
}
}
&__expand {
display: flex;
align-items: center;
cursor: pointer;
position: relative;
&__value {
font-size: 16px;
line-height: 21px;
color: #acbace;
margin-right: 10px;
font-family: 'font_regular', sans-serif;
font-weight: 700;
}
&__dropdown {
position: absolute;
top: 35px;
left: 0;
background-color: #fff;
z-index: 1000;
border: 1px solid #c5cbdb;
box-shadow: 0 8px 34px rgba(161, 173, 185, 0.41);
border-radius: 6px;
min-width: 250px;
&__item {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 12px 25px;
font-size: 14px;
line-height: 20px;
color: #7e8b9c;
cursor: pointer;
text-decoration: none;
&__name {
font-family: 'font_bold', sans-serif;
margin-left: 15px;
font-size: 14px;
line-height: 20px;
color: #7e8b9c;
}
&:hover {
background-color: #f2f2f6;
}
}
}
}
&__content-area {
width: 100%;
padding: 0 20px;
margin-bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
&__container {
width: 610px;
padding: 60px 80px;
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 20px;
box-sizing: border-box;
&__title-area {
display: flex;
justify-content: space-between;
align-items: center;
&__title {
font-size: 24px;
margin: 10px 0;
letter-spacing: -0.100741px;
color: #252525;
font-family: 'font_bold', sans-serif;
font-weight: 800;
}
}
&__input-wrapper {
margin-top: 20px;
}
&__button {
font-family: 'font_regular', sans-serif;
font-weight: 700;
margin-top: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: #376fff;
border-radius: 50px;
color: #fff;
cursor: pointer;
width: 100%;
height: 48px;
&:hover {
background-color: #0059d0;
}
}
}
&__login-container {
width: 100%;
align-items: center;
justify-content: center;
margin-top: 50px;
display: block;
text-align: center;
&__link {
font-family: 'font_medium', sans-serif;
text-decoration: none;
font-size: 14px;
line-height: 18px;
color: #376fff;
}
}
}
}
@media screen and (max-width: 750px) {
.forgot-area {
&__content-area {
&__container {
width: 100%;
}
}
&__expand {
&__dropdown {
left: -200px;
}
}
}
}
@media screen and (max-width: 414px) {
.forgot-area {
&__logo-wrapper {
margin: 40px;
}
&__content-area {
padding: 0;
&__container {
padding: 60px 60px;
border-radius: 0;
}
}
}
}
</style>
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="forgot-area" @keyup.enter="onSendConfigurations">
<div class="forgot-area__logo-wrapper">
<LogoIcon class="forgot-area__logo-wrapper__logo" @click="onLogoClick" />
</div>
<div class="forgot-area__content-area">
<div class="forgot-area__content-area__container">
<div class="forgot-area__content-area__container__title-area">
<h1 class="forgot-area__content-area__container__title-area__title">Reset Password</h1>
<div class="forgot-area__expand" @click.stop="toggleDropdown">
<span class="forgot-area__expand__value">{{ satelliteName }}</span>
<BottomArrowIcon />
<div v-if="isDropdownShown" v-click-outside="closeDropdown" class="forgot-area__expand__dropdown">
<div class="forgot-area__expand__dropdown__item" @click.stop="closeDropdown">
<SelectedCheckIcon />
<span class="forgot-area__expand__dropdown__item__name">{{ satelliteName }}</span>
</div>
<a v-for="sat in partneredSatellites" :key="sat.id" class="forgot-area__expand__dropdown__item" :href="sat.address + '/forgot-password'">
{{ sat.name }}
</a>
</div>
</div>
</div>
<p class="forgot-area__content-area__container__message">If youve forgotten your account password, you can reset it here. Make sure youre signing in to the right satellite.</p>
<div class="forgot-area__content-area__container__input-wrapper">
<HeaderlessInput
class="full-input"
label="Email Address"
placeholder="example@email.com"
:error="emailError"
width="calc(100% - 2px)"
height="46px"
@setData="setEmail"
/>
</div>
<p class="forgot-area__content-area__container__button" @click.prevent="onSendConfigurations">Reset Password</p>
</div>
<div class="forgot-area__content-area__login-container">
<router-link :to="loginPath" class="forgot-area__content-area__login-container__link">
Back to Login
</router-link>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import BottomArrowIcon from '@/../static/images/common/lightBottomArrow.svg';
import SelectedCheckIcon from '@/../static/images/common/selectedCheck.svg';
import LogoIcon from '@/../static/images/logo.svg';
import { AuthHttpApi } from '@/api/auth';
import { RouteConfig } from '@/router';
import { PartneredSatellite } from '@/types/common';
import { Validator } from '@/utils/validation';
// @vue/component
@Component({
components: {
HeaderlessInput,
BottomArrowIcon,
SelectedCheckIcon,
LogoIcon,
},
})
export default class ForgotPassword extends Vue {
private email = '';
private emailError = '';
private readonly auth: AuthHttpApi = new AuthHttpApi();
// tardigrade logic
public isDropdownShown = false;
public readonly loginPath: string = RouteConfig.Login.path;
/**
* Sets the email field to the given value.
*/
public setEmail(value: string): void {
this.email = value;
this.emailError = '';
}
/**
* Name of the current satellite.
*/
public get satelliteName(): string {
return this.$store.state.appStateModule.satelliteName;
}
/**
* Information about partnered satellites, including name and signup link.
*/
public get partneredSatellites(): PartneredSatellite[] {
return this.$store.state.appStateModule.partneredSatellites;
}
/**
* Toggles satellite selection dropdown visibility (Tardigrade).
*/
public toggleDropdown(): void {
this.isDropdownShown = !this.isDropdownShown;
}
/**
* Closes satellite selection dropdown (Tardigrade).
*/
public closeDropdown(): void {
this.isDropdownShown = false;
}
/**
* Sends recovery password email.
*/
public async onSendConfigurations(): Promise<void> {
if (!this.validateFields()) {
return;
}
try {
await this.auth.forgotPassword(this.email);
} catch (error) {
await this.$notify.error(error.message);
return;
}
await this.$notify.success('Please look for instructions at your email');
}
/**
* Changes location to Login route.
*/
public onBackToLoginClick(): void {
this.$router.push(RouteConfig.Login.path);
}
/**
* Reloads the page.
*/
public onLogoClick(): void {
location.reload();
}
/**
* Returns whether the email address is properly structured.
*/
private validateFields(): boolean {
const isEmailValid = Validator.email(this.email.trim());
if (!isEmailValid) {
this.emailError = 'Invalid Email';
}
return isEmailValid;
}
}
</script>
<style scoped lang="scss">
.forgot-area {
display: flex;
flex-direction: column;
justify-content: flex-start;
font-family: 'font_regular', sans-serif;
background-color: #f5f6fa;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-height: 100%;
overflow-y: scroll;
&__logo-wrapper {
text-align: center;
margin: 70px 0;
&__logo {
cursor: pointer;
}
}
&__expand {
display: flex;
align-items: center;
cursor: pointer;
position: relative;
&__value {
font-size: 16px;
line-height: 21px;
color: #acbace;
margin-right: 10px;
font-family: 'font_regular', sans-serif;
font-weight: 700;
}
&__dropdown {
position: absolute;
top: 35px;
left: 0;
background-color: #fff;
z-index: 1000;
border: 1px solid #c5cbdb;
box-shadow: 0 8px 34px rgba(161, 173, 185, 0.41);
border-radius: 6px;
min-width: 250px;
&__item {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 12px 25px;
font-size: 14px;
line-height: 20px;
color: #7e8b9c;
cursor: pointer;
text-decoration: none;
&__name {
font-family: 'font_bold', sans-serif;
margin-left: 15px;
font-size: 14px;
line-height: 20px;
color: #7e8b9c;
}
&:hover {
background-color: #f2f2f6;
}
}
}
}
&__content-area {
width: 100%;
padding: 0 20px;
margin-bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
&__container {
width: 610px;
padding: 60px 80px;
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 20px;
box-sizing: border-box;
&__title-area {
display: flex;
justify-content: space-between;
align-items: center;
&__title {
font-size: 24px;
margin: 10px 0;
letter-spacing: -0.100741px;
color: #252525;
font-family: 'font_bold', sans-serif;
font-weight: 800;
}
}
&__input-wrapper {
margin-top: 20px;
}
&__button {
font-family: 'font_regular', sans-serif;
font-weight: 700;
margin-top: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: #376fff;
border-radius: 50px;
color: #fff;
cursor: pointer;
width: 100%;
height: 48px;
&:hover {
background-color: #0059d0;
}
}
}
&__login-container {
width: 100%;
align-items: center;
justify-content: center;
margin-top: 50px;
display: block;
text-align: center;
&__link {
font-family: 'font_medium', sans-serif;
text-decoration: none;
font-size: 14px;
line-height: 18px;
color: #376fff;
}
}
}
}
@media screen and (max-width: 750px) {
.forgot-area {
&__content-area {
&__container {
width: 100%;
}
}
&__expand {
&__dropdown {
left: -200px;
}
}
}
}
@media screen and (max-width: 414px) {
.forgot-area {
&__logo-wrapper {
margin: 40px;
}
&__content-area {
padding: 0;
&__container {
padding: 60px 60px;
border-radius: 0;
}
}
}
}
</style>

View File

@ -1,344 +1,344 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="reset-area" @keyup.enter="onResetClick">
<div class="reset-area__logo-wrapper">
<LogoIcon class="reset-area__logo-wrapper_logo" @click="onLogoClick" />
</div>
<div class="reset-area__content-area">
<div class="reset-area__content-area__container" :class="{'success': isSuccessfulPasswordResetShown}">
<template v-if="!isSuccessfulPasswordResetShown">
<h1 class="reset-area__content-area__container__title">Reset Password</h1>
<p class="reset-area__content-area__container__message">Please enter your new password.</p>
<div class="reset-area__content-area__container__input-wrapper password">
<HeaderlessInput
label="Password"
placeholder="Enter Password"
:error="passwordError"
width="100%"
height="46px"
is-password="true"
@setData="setPassword"
@showPasswordStrength="showPasswordStrength"
@hidePasswordStrength="hidePasswordStrength"
/>
<PasswordStrength
:password-string="password"
:is-shown="isPasswordStrengthShown"
/>
</div>
<div class="reset-area__content-area__container__input-wrapper">
<HeaderlessInput
label="Retype Password"
placeholder="Retype Password"
:error="repeatedPasswordError"
width="100%"
height="46px"
is-password="true"
@setData="setRepeatedPassword"
/>
</div>
<p class="reset-area__content-area__container__button" @click.prevent="onResetClick">Reset Password</p>
</template>
<template v-else>
<KeyIcon />
<h2 class="reset-area__content-area__container__title success">Success!</h2>
<p class="reset-area__content-area__container__sub-title">
You have successfully changed your password.
</p>
</template>
</div>
<router-link :to="loginPath" class="reset-area__content-area__login-link">
Back to Login
</router-link>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import PasswordStrength from '@/components/common/PasswordStrength.vue';
import LogoIcon from '@/../static/images/logo.svg';
import KeyIcon from '@/../static/images/resetPassword/success.svg';
import { AuthHttpApi } from '@/api/auth';
import { RouteConfig } from '@/router';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { Validator } from '@/utils/validation';
// @vue/component
@Component({
components: {
LogoIcon,
HeaderlessInput,
PasswordStrength,
KeyIcon,
},
})
export default class ResetPassword extends Vue {
private token = '';
private password = '';
private repeatedPassword = '';
private passwordError = '';
private repeatedPasswordError = '';
private isLoading = false;
private readonly auth: AuthHttpApi = new AuthHttpApi();
public isPasswordStrengthShown = false;
public readonly loginPath: string = RouteConfig.Login.path;
/**
* Lifecycle hook on component destroy.
* Sets view to default state.
*/
public beforeDestroy(): void {
if (this.isSuccessfulPasswordResetShown) {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET);
}
}
/**
* Lifecycle hook after initial render.
* Initializes recovery token from route param
* and redirects to login if token doesn't exist.
*/
public mounted(): void {
if (this.$route.query.token) {
this.token = this.$route.query.token.toString();
} else {
this.$router.push(RouteConfig.Login.path);
}
}
/**
* Returns whether the successful password reset area is shown.
*/
public get isSuccessfulPasswordResetShown() : boolean {
return this.$store.state.appStateModule.appState.isSuccessfulPasswordResetShown;
}
/**
* Validates input fields and requests password reset.
*/
public async onResetClick(): Promise<void> {
if (this.isLoading) {
return;
}
this.isLoading = true;
if (!this.validateFields()) {
this.isLoading = false;
return;
}
try {
await this.auth.resetPassword(this.token, this.password);
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET);
} catch (error) {
await this.$notify.error(error.message);
}
this.isLoading = false;
}
/**
* Validates input values to satisfy expected rules.
*/
private validateFields(): boolean {
let isNoErrors = true;
if (!Validator.password(this.password)) {
this.passwordError = 'Invalid password';
isNoErrors = false;
}
if (this.repeatedPassword !== this.password) {
this.repeatedPasswordError = 'Password doesn\'t match';
isNoErrors = false;
}
return isNoErrors;
}
/**
* Makes password strength container visible.
*/
public showPasswordStrength(): void {
this.isPasswordStrengthShown = true;
}
/**
* Hides password strength container.
*/
public hidePasswordStrength(): void {
this.isPasswordStrengthShown = false;
}
/**
* Reloads the page.
*/
public onLogoClick(): void {
location.reload();
}
/**
* Sets user's password field from value string.
*/
public setPassword(value: string): void {
this.password = value.trim();
this.passwordError = '';
}
/**
* Sets user's repeat password field from value string.
*/
public setRepeatedPassword(value: string): void {
this.repeatedPassword = value.trim();
this.repeatedPasswordError = '';
}
}
</script>
<style scoped lang="scss">
.reset-area {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
font-family: 'font_regular', sans-serif;
background-color: #f5f6fa;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-height: 100%;
overflow-y: scroll;
&__logo-wrapper {
text-align: center;
margin: 70px 0;
&__logo {
cursor: pointer;
}
}
&__content-area {
width: 100%;
padding: 0 20px;
margin-bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
&__container {
width: 610px;
padding: 60px 80px;
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 20px;
box-sizing: border-box;
&.success {
align-items: center;
text-align: center;
}
&__input-wrapper {
margin-top: 20px;
&.password {
position: relative;
}
}
&__title {
font-size: 24px;
margin: 10px 0;
letter-spacing: -0.100741px;
color: #252525;
font-family: 'font_bold', sans-serif;
font-weight: 800;
&.success {
font-size: 40px;
margin: 25px 0;
}
}
&__button {
font-family: 'font_regular', sans-serif;
font-weight: 700;
margin-top: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: #376fff;
border-radius: 50px;
color: #fff;
cursor: pointer;
width: 100%;
height: 48px;
&:hover {
background-color: #0059d0;
}
}
}
&__login-link {
font-family: 'font_medium', sans-serif;
text-decoration: none;
font-size: 14px;
line-height: 18px;
color: #376fff;
margin-top: 50px;
}
}
}
@media screen and (max-width: 750px) {
.reset-area {
&__content-area {
&__container {
width: 100%;
}
}
}
}
@media screen and (max-width: 414px) {
.reset-area {
&__logo-wrapper {
margin: 40px;
}
&__content-area {
padding: 0;
&__container {
padding: 60px 60px;
border-radius: 0;
}
}
}
}
</style>
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="reset-area" @keyup.enter="onResetClick">
<div class="reset-area__logo-wrapper">
<LogoIcon class="reset-area__logo-wrapper_logo" @click="onLogoClick" />
</div>
<div class="reset-area__content-area">
<div class="reset-area__content-area__container" :class="{'success': isSuccessfulPasswordResetShown}">
<template v-if="!isSuccessfulPasswordResetShown">
<h1 class="reset-area__content-area__container__title">Reset Password</h1>
<p class="reset-area__content-area__container__message">Please enter your new password.</p>
<div class="reset-area__content-area__container__input-wrapper password">
<HeaderlessInput
label="Password"
placeholder="Enter Password"
:error="passwordError"
width="100%"
height="46px"
is-password="true"
@setData="setPassword"
@showPasswordStrength="showPasswordStrength"
@hidePasswordStrength="hidePasswordStrength"
/>
<PasswordStrength
:password-string="password"
:is-shown="isPasswordStrengthShown"
/>
</div>
<div class="reset-area__content-area__container__input-wrapper">
<HeaderlessInput
label="Retype Password"
placeholder="Retype Password"
:error="repeatedPasswordError"
width="100%"
height="46px"
is-password="true"
@setData="setRepeatedPassword"
/>
</div>
<p class="reset-area__content-area__container__button" @click.prevent="onResetClick">Reset Password</p>
</template>
<template v-else>
<KeyIcon />
<h2 class="reset-area__content-area__container__title success">Success!</h2>
<p class="reset-area__content-area__container__sub-title">
You have successfully changed your password.
</p>
</template>
</div>
<router-link :to="loginPath" class="reset-area__content-area__login-link">
Back to Login
</router-link>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import PasswordStrength from '@/components/common/PasswordStrength.vue';
import LogoIcon from '@/../static/images/logo.svg';
import KeyIcon from '@/../static/images/resetPassword/success.svg';
import { AuthHttpApi } from '@/api/auth';
import { RouteConfig } from '@/router';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { Validator } from '@/utils/validation';
// @vue/component
@Component({
components: {
LogoIcon,
HeaderlessInput,
PasswordStrength,
KeyIcon,
},
})
export default class ResetPassword extends Vue {
private token = '';
private password = '';
private repeatedPassword = '';
private passwordError = '';
private repeatedPasswordError = '';
private isLoading = false;
private readonly auth: AuthHttpApi = new AuthHttpApi();
public isPasswordStrengthShown = false;
public readonly loginPath: string = RouteConfig.Login.path;
/**
* Lifecycle hook on component destroy.
* Sets view to default state.
*/
public beforeDestroy(): void {
if (this.isSuccessfulPasswordResetShown) {
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET);
}
}
/**
* Lifecycle hook after initial render.
* Initializes recovery token from route param
* and redirects to login if token doesn't exist.
*/
public mounted(): void {
if (this.$route.query.token) {
this.token = this.$route.query.token.toString();
} else {
this.$router.push(RouteConfig.Login.path);
}
}
/**
* Returns whether the successful password reset area is shown.
*/
public get isSuccessfulPasswordResetShown() : boolean {
return this.$store.state.appStateModule.appState.isSuccessfulPasswordResetShown;
}
/**
* Validates input fields and requests password reset.
*/
public async onResetClick(): Promise<void> {
if (this.isLoading) {
return;
}
this.isLoading = true;
if (!this.validateFields()) {
this.isLoading = false;
return;
}
try {
await this.auth.resetPassword(this.token, this.password);
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET);
} catch (error) {
await this.$notify.error(error.message);
}
this.isLoading = false;
}
/**
* Validates input values to satisfy expected rules.
*/
private validateFields(): boolean {
let isNoErrors = true;
if (!Validator.password(this.password)) {
this.passwordError = 'Invalid password';
isNoErrors = false;
}
if (this.repeatedPassword !== this.password) {
this.repeatedPasswordError = 'Password doesn\'t match';
isNoErrors = false;
}
return isNoErrors;
}
/**
* Makes password strength container visible.
*/
public showPasswordStrength(): void {
this.isPasswordStrengthShown = true;
}
/**
* Hides password strength container.
*/
public hidePasswordStrength(): void {
this.isPasswordStrengthShown = false;
}
/**
* Reloads the page.
*/
public onLogoClick(): void {
location.reload();
}
/**
* Sets user's password field from value string.
*/
public setPassword(value: string): void {
this.password = value.trim();
this.passwordError = '';
}
/**
* Sets user's repeat password field from value string.
*/
public setRepeatedPassword(value: string): void {
this.repeatedPassword = value.trim();
this.repeatedPasswordError = '';
}
}
</script>
<style scoped lang="scss">
.reset-area {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
font-family: 'font_regular', sans-serif;
background-color: #f5f6fa;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
min-height: 100%;
overflow-y: scroll;
&__logo-wrapper {
text-align: center;
margin: 70px 0;
&__logo {
cursor: pointer;
}
}
&__content-area {
width: 100%;
padding: 0 20px;
margin-bottom: 50px;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
&__container {
width: 610px;
padding: 60px 80px;
display: flex;
flex-direction: column;
background-color: #fff;
border-radius: 20px;
box-sizing: border-box;
&.success {
align-items: center;
text-align: center;
}
&__input-wrapper {
margin-top: 20px;
&.password {
position: relative;
}
}
&__title {
font-size: 24px;
margin: 10px 0;
letter-spacing: -0.100741px;
color: #252525;
font-family: 'font_bold', sans-serif;
font-weight: 800;
&.success {
font-size: 40px;
margin: 25px 0;
}
}
&__button {
font-family: 'font_regular', sans-serif;
font-weight: 700;
margin-top: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: #376fff;
border-radius: 50px;
color: #fff;
cursor: pointer;
width: 100%;
height: 48px;
&:hover {
background-color: #0059d0;
}
}
}
&__login-link {
font-family: 'font_medium', sans-serif;
text-decoration: none;
font-size: 14px;
line-height: 18px;
color: #376fff;
margin-top: 50px;
}
}
}
@media screen and (max-width: 750px) {
.reset-area {
&__content-area {
&__container {
width: 100%;
}
}
}
}
@media screen and (max-width: 414px) {
.reset-area {
&__logo-wrapper {
margin: 40px;
}
&__content-area {
padding: 0;
&__container {
padding: 60px 60px;
border-radius: 0;
}
}
}
}
</style>

View File

@ -14,7 +14,10 @@ module.exports = {
parserOptions: {
ecmaVersion: 2020
},
plugins: ["storj"],
rules: {
"linebreak-style": ["error", "unix"],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
@ -43,5 +46,7 @@ module.exports = {
"vue/no-useless-v-bind": ["warn"],
'vue/no-unregistered-components': ['warn', { ignorePatterns: ['router-link', 'router-view'] }],
'storj/vue/require-annotation': 'warn',
},
}

View File

@ -0,0 +1,11 @@
*.*
!*.vue
!*.css
!*.sss
!*.less
!*.scss
!*.sass
dist
node_modules
coverage

View File

@ -11,7 +11,6 @@ module.exports = {
"stylelint-scss"
],
"extends": "stylelint-config-standard",
"ignoreFiles": ["dist/**"],
"rules": {
"indentation": 4,
"string-quotes": "single",

View File

@ -35,6 +35,7 @@
"compression-webpack-plugin": "6.0.0",
"core-js": "3.6.5",
"eslint": "6.7.2",
"eslint-plugin-storj": "file:../eslint-storj",
"eslint-plugin-vue": "7.16.0",
"jest-fetch-mock": "3.0.0",
"sass": "1.37.0",
@ -51,6 +52,11 @@
"vue-template-compiler": "2.6.11"
}
},
"../eslint-storj": {
"name": "eslint-plugin-storj",
"version": "1.0.0",
"dev": true
},
"node_modules/@babel/code-frame": {
"version": "7.14.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
@ -10730,6 +10736,10 @@
"rimraf": "bin.js"
}
},
"node_modules/eslint-plugin-storj": {
"resolved": "../eslint-storj",
"link": true
},
"node_modules/eslint-plugin-vue": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.16.0.tgz",
@ -36231,6 +36241,9 @@
}
}
},
"eslint-plugin-storj": {
"version": "file:../eslint-storj"
},
"eslint-plugin-vue": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.16.0.tgz",

View File

@ -4,8 +4,8 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"lint": "vue-cli-service lint --max-warnings 0 --fix && stylelint --max-warnings 0 \"**/*.{vue,css,sss,less,scss,sass}\" --fix",
"lint-ci": "vue-cli-service lint --max-warnings 0 --no-fix && stylelint --max-warnings 0 --no-fix \"**/*.{vue,css,sss,less,scss,sass}\"",
"lint": "vue-cli-service lint --max-warnings 0 --fix && stylelint . --max-warnings 0 --fix",
"lint-ci": "vue-cli-service lint --max-warnings 0 --no-fix && stylelint . --max-warnings 0 --no-fix",
"build": "vue-cli-service build",
"dev": "vue-cli-service build --mode development --watch",
"test": "vue-cli-service test:unit"
@ -39,6 +39,7 @@
"core-js": "3.6.5",
"eslint": "6.7.2",
"eslint-plugin-vue": "7.16.0",
"eslint-plugin-storj": "file:../eslint-storj",
"jest-fetch-mock": "3.0.0",
"sass": "1.37.0",
"sass-loader": "8.0.0",