web/satellite,satellite/console: Overhaul password reset

Updates the password reset page to use the new theme.
Adds new endpoint '/api/v0/auth/reset-password'
for password reset.

Additionally, updates the link-clicking mail simulator to only
click links with a specified attribute. Otherwise, the password reset
cancellation link would be clicked before the password reset link
could be accessed, rendering testing impossible.

Change-Id: I8fde74ef7ad980880a7bf6558e3b9ed31509a393
This commit is contained in:
Jeremy Wharton 2021-07-23 15:53:19 -05:00 committed by Jeremy Wharton
parent f807518477
commit 51ebc564d9
23 changed files with 601 additions and 928 deletions

1
go.mod
View File

@ -47,6 +47,7 @@ require (
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1

View File

@ -492,7 +492,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
ServerAddress: mailConfig.SMTPServerAddress,
}
default:
sender = &simulate.LinkClicker{}
sender = simulate.NewDefaultLinkClicker()
}
peer.Mail.Service, err = mailservice.New(

View File

@ -550,6 +550,28 @@ func (a *Auth) GenerateMFARecoveryCodes(w http.ResponseWriter, r *http.Request)
}
}
// ResetPassword resets user's password using recovery token.
func (a *Auth) ResetPassword(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
var resetPassword struct {
RecoveryToken string `json:"token"`
NewPassword string `json:"password"`
}
err = json.NewDecoder(r.Body).Decode(&resetPassword)
if err != nil {
a.serveJSONError(w, err)
}
err = a.service.ResetPassword(ctx, resetPassword.RecoveryToken, resetPassword.NewPassword, time.Now())
if err != nil {
a.serveJSONError(w, err)
}
}
// serveJSONError writes JSON error to response output stream.
func (a *Auth) serveJSONError(w http.ResponseWriter, err error) {
status := a.getStatusCode(err)
@ -561,7 +583,7 @@ func (a *Auth) getStatusCode(err error) int {
switch {
case console.ErrValidation.Has(err), console.ErrRecaptcha.Has(err):
return http.StatusBadRequest
case console.ErrUnauthorized.Has(err):
case console.ErrUnauthorized.Has(err), console.ErrRecoveryToken.Has(err):
return http.StatusUnauthorized
case console.ErrEmailUsed.Has(err):
return http.StatusConflict
@ -583,6 +605,11 @@ func (a *Auth) getUserErrorMessage(err error) string {
return "We are unable to create your account. This is an invite-only alpha, please join our waitlist to receive an invitation"
case console.ErrEmailUsed.Has(err):
return "This email is already in use; try another"
case console.ErrRecoveryToken.Has(err):
if console.ErrTokenExpiration.Has(err) {
return "The recovery token has expired"
}
return "The recovery token is invalid"
case errors.Is(err, errNotImplemented):
return "The server is incapable of fulfilling the request"
default:

View File

@ -396,3 +396,47 @@ func TestMFAEndpoints(t *testing.T) {
require.NoError(t, result.Body.Close())
})
}
func TestResetPasswordEndpoint(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
newPass := "123a123"
sat := planet.Satellites[0]
user, err := sat.AddUser(ctx, console.CreateUser{
FullName: "Test User",
Email: "test@mail.test",
}, 1)
require.NoError(t, err)
token, err := sat.DB.Console().ResetPasswordTokens().Create(ctx, user.ID)
require.NoError(t, err)
require.NotNil(t, token)
tokenStr := token.Secret.String()
tryReset := func(token, password string) int {
url := "http://" + sat.API.Console.Listener.Addr().String() + "/api/v0/auth/reset-password"
bodyBytes, err := json.Marshal(map[string]string{
"password": password,
"token": token,
})
require.NoError(t, err)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(bodyBytes))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
result, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, result.Body.Close())
return result.StatusCode
}
require.Equal(t, http.StatusUnauthorized, tryReset("badToken", newPass))
require.Equal(t, http.StatusBadRequest, tryReset(tokenStr, "bad"))
require.Equal(t, http.StatusOK, tryReset(tokenStr, newPass))
})
}

View File

@ -157,8 +157,6 @@ type Server struct {
notFound *template.Template
internalServerError *template.Template
usageReport *template.Template
resetPassword *template.Template
success *template.Template
}
}
@ -231,6 +229,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
authRouter.Handle("/register", server.rateLimiter.Limit(http.HandlerFunc(authController.Register))).Methods(http.MethodPost)
authRouter.Handle("/forgot-password/{email}", server.rateLimiter.Limit(http.HandlerFunc(authController.ForgotPassword))).Methods(http.MethodPost)
authRouter.Handle("/resend-email/{id}", server.rateLimiter.Limit(http.HandlerFunc(authController.ResendEmail))).Methods(http.MethodPost)
authRouter.Handle("/reset-password", server.rateLimiter.Limit(http.HandlerFunc(authController.ResetPassword))).Methods(http.MethodPost)
paymentController := consoleapi.NewPayments(logger, service)
paymentsRouter := router.PathPrefix("/api/v0/payments").Subrouter()
@ -264,7 +263,6 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
if server.config.StaticDir != "" {
router.HandleFunc("/activation/", server.accountActivationHandler)
router.HandleFunc("/password-recovery/", server.passwordRecoveryHandler)
router.HandleFunc("/cancel-password-recovery/", server.cancelPasswordRecoveryHandler)
router.HandleFunc("/usage-report", server.bucketUsageReportHandler)
router.PathPrefix("/static/").Handler(server.brotliMiddleware(http.StripPrefix("/static", fs)))
@ -572,58 +570,6 @@ func (server *Server) accountActivationHandler(w http.ResponseWriter, r *http.Re
http.Redirect(w, r, server.config.AccountActivationRedirectURL, http.StatusTemporaryRedirect)
}
func (server *Server) passwordRecoveryHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
recoveryToken := r.URL.Query().Get("token")
if len(recoveryToken) == 0 {
server.serveError(w, http.StatusNotFound)
return
}
var data struct {
SatelliteName string
}
data.SatelliteName = server.config.SatelliteName
switch r.Method {
case http.MethodPost:
err := r.ParseForm()
if err != nil {
server.serveError(w, http.StatusNotFound)
return
}
password := r.FormValue("password")
passwordRepeat := r.FormValue("passwordRepeat")
if strings.Compare(password, passwordRepeat) != 0 {
server.serveError(w, http.StatusNotFound)
return
}
err = server.service.ResetPassword(ctx, recoveryToken, password)
if err != nil {
server.serveError(w, http.StatusNotFound)
return
}
if err := server.templates.success.Execute(w, data); err != nil {
server.log.Error("success reset password template could not be executed", zap.Error(Error.Wrap(err)))
return
}
case http.MethodGet:
if err := server.templates.resetPassword.Execute(w, data); err != nil {
server.log.Error("reset password template could not be executed", zap.Error(Error.Wrap(err)))
return
}
default:
server.serveError(w, http.StatusNotFound)
return
}
}
func (server *Server) cancelPasswordRecoveryHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer mon.Task()(&ctx)(nil)
@ -819,16 +765,6 @@ func (server *Server) initializeTemplates() (err error) {
server.log.Error("dist folder is not generated. use 'npm run build' command", zap.Error(err))
}
server.templates.success, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "resetPassword", "success.html"))
if err != nil {
return Error.Wrap(err)
}
server.templates.resetPassword, err = template.ParseFiles(filepath.Join(server.config.StaticDir, "static", "resetPassword", "resetPassword.html"))
if err != nil {
return Error.Wrap(err)
}
server.templates.usageReport, err = template.ParseFiles(path.Join(server.config.StaticDir, "static", "reports", "usageReport.html"))
if err != nil {
return Error.Wrap(err)

View File

@ -36,8 +36,11 @@ var mon = monkit.Package()
const (
// maxLimit specifies the limit for all paged queries.
maxLimit = 50
tokenExpirationTime = 24 * time.Hour
maxLimit = 50
// TokenExpirationTime specifies the expiration time for
// auth tokens, account recovery tokens, and activation tokens.
TokenExpirationTime = 24 * time.Hour
// TestPasswordCost is the hashing complexity to use for testing.
TestPasswordCost = bcrypt.MinCost
@ -87,6 +90,9 @@ var (
// ErrRecaptcha describes reCAPTCHA validation errors.
ErrRecaptcha = errs.Class("recaptcha validation")
// ErrRecoveryToken describes account recovery token errors.
ErrRecoveryToken = errs.Class("recovery token")
)
// Service is handling accounts related logic.
@ -746,7 +752,7 @@ func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (
now := time.Now()
if now.After(user.CreatedAt.Add(tokenExpirationTime)) {
if now.After(user.CreatedAt.Add(TokenExpirationTime)) {
return ErrTokenExpiration.Wrap(err)
}
@ -763,16 +769,16 @@ func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (
}
// ResetPassword - is a method for resetting user password.
func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, password string) (err error) {
func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, password string, t time.Time) (err error) {
defer mon.Task()(&ctx)(&err)
secret, err := ResetPasswordSecretFromBase64(resetPasswordToken)
if err != nil {
return Error.Wrap(err)
return ErrRecoveryToken.Wrap(err)
}
token, err := s.store.ResetPasswordTokens().GetBySecret(ctx, secret)
if err != nil {
return Error.Wrap(err)
return ErrRecoveryToken.Wrap(err)
}
user, err := s.store.Users().Get(ctx, *token.OwnerID)
@ -784,8 +790,8 @@ func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, passwor
return Error.Wrap(err)
}
if time.Since(token.CreatedAt) > tokenExpirationTime {
return ErrTokenExpiration.New(passwordRecoveryTokenIsExpiredErrMsg)
if t.Sub(token.CreatedAt) > TokenExpirationTime {
return ErrRecoveryToken.Wrap(ErrTokenExpiration.New(passwordRecoveryTokenIsExpiredErrMsg))
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), s.config.PasswordCost)
@ -870,7 +876,7 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token string, er
claims := consoleauth.Claims{
ID: user.ID,
Expiration: time.Now().Add(tokenExpirationTime),
Expiration: time.Now().Add(TokenExpirationTime),
}
token, err = s.createToken(ctx, &claims)

View File

@ -461,3 +461,40 @@ func TestHasCouponApplied(t *testing.T) {
require.True(t, hasCoupon)
})
}
func TestResetPassword(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
newPass := "123a123"
sat := planet.Satellites[0]
service := sat.API.Console.Service
user, err := sat.AddUser(ctx, console.CreateUser{
FullName: "Test User",
Email: "test@mail.test",
}, 1)
require.NoError(t, err)
token, err := sat.DB.Console().ResetPasswordTokens().Create(ctx, user.ID)
require.NoError(t, err)
require.NotNil(t, token)
tokenStr := token.Secret.String()
// Expect error when providing bad token.
err = service.ResetPassword(ctx, "badToken", newPass, token.CreatedAt)
require.True(t, console.ErrRecoveryToken.Has(err))
// Expect error when providing good but expired token.
err = service.ResetPassword(ctx, tokenStr, newPass, token.CreatedAt.Add(console.TokenExpirationTime).Add(time.Second))
require.True(t, console.ErrTokenExpiration.Has(err))
// Expect error when providing good token with bad (too short) password.
err = service.ResetPassword(ctx, tokenStr, "bad", token.CreatedAt)
require.True(t, console.ErrValidation.Has(err))
// Expect success when providing good token and good password.
err = service.ResetPassword(ctx, tokenStr, newPass, token.CreatedAt)
require.NoError(t, err)
})
}

View File

@ -6,11 +6,11 @@ package simulate
import (
"context"
"net/http"
"regexp"
"strings"
"github.com/spacemonkeygo/monkit/v3"
"github.com/zeebo/errs"
"golang.org/x/net/html"
"storj.io/storj/private/post"
"storj.io/storj/satellite/mailservice"
@ -23,35 +23,35 @@ var _ mailservice.Sender = (*LinkClicker)(nil)
// LinkClicker is mailservice.Sender that click all links from html msg parts.
//
// architecture: Service
type LinkClicker struct{}
type LinkClicker struct {
// MarkerAttribute specifies the attribute every anchor element must have in order to be clicked.
// This prevents the link clicker from clicking links that it should not (such as the password reset cancellation link).
// Leaving this field empty will make it click every link.
MarkerAttribute string
}
// NewDefaultLinkClicker returns a LinkClicker with the default marker attribute.
func NewDefaultLinkClicker() *LinkClicker {
return &LinkClicker{MarkerAttribute: "data-simulate"}
}
// FromAddress return empty mail address.
func (clicker *LinkClicker) FromAddress() post.Address {
return post.Address{}
}
// SendEmail click all links from email html parts.
// SendEmail click all links belonging to properly attributed anchors from email html parts.
func (clicker *LinkClicker) SendEmail(ctx context.Context, msg *post.Message) (err error) {
defer mon.Task()(&ctx)(&err)
// dirty way to find links without pulling in a html dependency
regx := regexp.MustCompile(`href="([^\s])+"`)
// collect all links
var links []string
var body string
for _, part := range msg.Parts {
tags := findLinkTags(part.Content)
for _, tag := range tags {
href := regx.FindString(tag)
if href == "" {
continue
}
links = append(links, href[len(`href="`):len(href)-1])
}
body += part.Content
}
// click all links
var sendError error
for _, link := range links {
for _, link := range clicker.FindLinks(body) {
req, err := http.NewRequestWithContext(ctx, "GET", link, nil)
if err != nil {
continue
@ -66,24 +66,31 @@ func (clicker *LinkClicker) SendEmail(ctx context.Context, msg *post.Message) (e
return sendError
}
func findLinkTags(body string) []string {
var tags []string
// FindLinks returns a list of all links belonging to properly attributed anchors in the HTML body.
func (clicker *LinkClicker) FindLinks(body string) (links []string) {
tokens := html.NewTokenizer(strings.NewReader(body))
Loop:
for {
stTag := strings.Index(body, "<a")
if stTag < 0 {
switch tokens.Next() {
case html.ErrorToken:
break Loop
case html.StartTagToken:
token := tokens.Token()
if strings.ToLower(token.Data) == "a" {
simulate := clicker.MarkerAttribute == ""
var href string
for _, attr := range token.Attr {
if strings.ToLower(attr.Key) == "href" {
href = attr.Val
} else if !simulate && attr.Key == clicker.MarkerAttribute {
simulate = true
}
}
if simulate && href != "" {
links = append(links, href)
}
}
}
stripped := body[stTag:]
endTag := strings.Index(stripped, "</a>")
if endTag < 0 {
break Loop
}
offset := endTag + len("</a>") + 1
body = stripped[offset:]
tags = append(tags, stripped[:offset])
}
return tags
return links
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package simulate_test
import (
"testing"
"github.com/stretchr/testify/require"
"storj.io/storj/satellite/mailservice/simulate"
)
func TestFindLinks(t *testing.T) {
data := `
<a href="link1" data-simulate>
<A HREF="link2" data-simulate>
<a href="link3">
<a href>
<a data-simulate>
`
clicker := simulate.LinkClicker{MarkerAttribute: "data-simulate"}
require.ElementsMatch(t, []string{"link1", "link2"}, clicker.FindLinks(data))
clicker.MarkerAttribute = ""
require.ElementsMatch(t, []string{"link1", "link2", "link3"}, clicker.FindLinks(data))
}

View File

@ -62,7 +62,6 @@ docker run -p 8080:8080 storjlabs/satellite-ui:latest
- [fonts](./fonts "fonts") folder: contains Inter font sets in ttf format.
- [images](./images "images") folder: contains illustrations.
- [reports](./reports "reports") folder: contains usage report table template.
- [resetPassword](./resetPassword "resetPassword") folder: contains page template for password reset and success page that appears after.
### tests
- [unit](./unit "unit") folder: contains project unit tests.
### Configuration files

View File

@ -345,4 +345,35 @@ export class AuthHttpApi {
throw new Error('Can not generate MFA recovery codes. Please try again later');
}
/**
* Used to reset user's password.
*
* @throws Error
*/
public async resetPassword(token: string, password: string): Promise<void> {
const path = `${this.ROOT_PATH}/reset-password`;
const body = {
token: token,
password: password,
};
const response = await this.http.post(path, JSON.stringify(body));
if (response.ok) {
return;
}
const result = await response.json();
const errMsg = result.error || 'Cannot reset password';
switch (response.status) {
case 400:
throw new ErrorBadRequest(errMsg);
case 401:
throw new ErrorUnauthorized(errMsg);
default:
throw new Error(errMsg);
}
}
}

View File

@ -43,6 +43,7 @@ const DashboardArea = () => import('@/views/DashboardArea.vue');
const ForgotPassword = () => import('@/views/ForgotPassword.vue');
const LoginArea = () => import('@/views/LoginArea.vue');
const RegisterArea = () => import('@/views/RegisterArea.vue');
const ResetPassword = () => import('@/views/ResetPassword.vue');
Vue.use(Router);
@ -55,6 +56,7 @@ export abstract class RouteConfig {
public static Login = new NavigationLink('/login', 'Login');
public static Register = new NavigationLink('/signup', 'Register');
public static ForgotPassword = new NavigationLink('/forgot-password', 'Forgot Password');
public static ResetPassword = new NavigationLink('/password-recovery', 'Reset Password');
public static Account = new NavigationLink('/account', 'Account');
public static ProjectDashboard = new NavigationLink('/project-dashboard', 'Dashboard');
public static Users = new NavigationLink('/project-members', 'Users');
@ -105,6 +107,7 @@ export const notProjectRelatedRoutes = [
RouteConfig.Login.name,
RouteConfig.Register.name,
RouteConfig.ForgotPassword.name,
RouteConfig.ResetPassword.name,
RouteConfig.Billing.name,
RouteConfig.BillingHistory.name,
RouteConfig.DepositHistory.name,
@ -130,6 +133,11 @@ export const router = new Router({
name: RouteConfig.ForgotPassword.name,
component: ForgotPassword,
},
{
path: RouteConfig.ResetPassword.path,
name: RouteConfig.ResetPassword.name,
component: ResetPassword,
},
{
path: RouteConfig.Root.path,
meta: {

View File

@ -27,6 +27,7 @@ class ViewsState {
public isChangePasswordPopupShown = false,
public isPaymentSelectionShown = false,
public isUploadCancelPopupVisible = false,
public isSuccessfulPasswordResetShown = false,
public setDefaultPaymentMethodID: string = "",
public deletePaymentMethodID: string = "",
@ -100,6 +101,10 @@ export const appStateModule = {
[APP_STATE_MUTATIONS.TOGGLE_SUCCESSFUL_REGISTRATION](state: State): void {
state.appState.isSuccessfulRegistrationShown = !state.appState.isSuccessfulRegistrationShown;
},
// Mutation changing 'successful password reset' area visibility.
[APP_STATE_MUTATIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET](state: State): void {
state.appState.isSuccessfulPasswordResetShown = !state.appState.isSuccessfulPasswordResetShown;
},
[APP_STATE_MUTATIONS.TOGGLE_CHANGE_PASSWORD_POPUP](state: State): void {
state.appState.isChangePasswordPopupShown = !state.appState.isChangePasswordPopupShown;
},
@ -243,6 +248,13 @@ export const appStateModule = {
commit(APP_STATE_MUTATIONS.TOGGLE_SUCCESSFUL_REGISTRATION);
},
[APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET]: function ({commit, state}: AppContext): void {
if (!state.appState.isSuccessfulPasswordResetShown) {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);
}
commit(APP_STATE_MUTATIONS.TOGGLE_SUCCESSFUL_PASSWORD_RESET);
},
[APP_STATE_ACTIONS.TOGGLE_CHANGE_PASSWORD_POPUP]: function ({commit}: AppContext): void {
commit(APP_STATE_MUTATIONS.TOGGLE_CHANGE_PASSWORD_POPUP);
},

View File

@ -22,6 +22,7 @@ export const APP_STATE_MUTATIONS = {
TOGGLE_AVAILABLE_BALANCE_DROPDOWN: 'TOGGLE_AVAILABLE_BALANCE_DROPDOWN',
TOGGLE_PERIODS_DROPDOWN: 'TOGGLE_PERIODS_DROPDOWN',
TOGGLE_SUCCESSFUL_REGISTRATION: 'TOGGLE_SUCCESSFUL_REGISTRATION',
TOGGLE_SUCCESSFUL_PASSWORD_RESET: 'TOGGLE_SUCCESSFUL_PASSWORD_RESET',
TOGGLE_SUCCESSFUL_PROJECT_CREATION_POPUP: 'TOGGLE_SUCCESSFUL_PROJECT_CREATION_POPUP',
TOGGLE_EDIT_PROFILE_POPUP: 'TOGGLE_EDIT_PROFILE_POPUP',
TOGGLE_CHANGE_PASSWORD_POPUP: 'TOGGLE_CHANGE_PASSWORD_POPUP',

View File

@ -14,6 +14,7 @@ export const APP_STATE_ACTIONS = {
TOGGLE_DEL_PROJ: 'toggleDeleteProjectPopup',
TOGGLE_DEL_ACCOUNT: 'toggleDeleteAccountPopup',
TOGGLE_SUCCESSFUL_REGISTRATION: 'TOGGLE_SUCCESSFUL_REGISTRATION',
TOGGLE_SUCCESSFUL_PASSWORD_RESET: 'TOGGLE_SUCCESSFUL_PASSWORD_RESET',
TOGGLE_SUCCESSFUL_PROJECT_CREATION_POPUP: 'toggleSuccessfulProjectCreationPopup',
TOGGLE_EDIT_PROFILE_POPUP: 'toggleEditProfilePopup',
TOGGLE_CHANGE_PASSWORD_POPUP: 'toggleChangePasswordPopup',

View File

@ -0,0 +1,343 @@
// 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/dcs-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';
@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

@ -166,7 +166,7 @@
</p>
<p style="font-size: 12px; line-height: 1.2; mso-line-height-alt: 14px; margin: 20px 0;">
<span>
<a style="font-family: 'Roboto', Tahoma, Verdana, Segoe, sans-serif;
<a data-simulate style="font-family: 'Roboto', Tahoma, Verdana, Segoe, sans-serif;
font-weight: bold; font-size: 16px; color: #ffffff; background-color: #2683FF;
padding: 12px 24px; border: none; border-radius: 4px; text-decoration: none;"
href="{{ .ActivationLink }}">

View File

@ -0,0 +1,10 @@
<svg width="207" height="89" version="1.1" viewBox="0 0 207 89" xmlns="http://www.w3.org/2000/svg">
<path d="m39 16c-4.4183 0-8-3.5817-8-8 0-4.41828 3.5817-8 8-8s8 3.58172 8 8c0 4.4183-3.5817 8-8 8z" fill="#FF69A1"/>
<path d="M8.10494 20L4.95911e-05 25.8885L3.09584 35.4164H13.114L16.2098 25.8885L8.10494 20Z" clip-rule="evenodd" fill="#276CFF" fill-rule="evenodd"/>
<path d="m198.105 28-8.105 5.8885 3.096 9.5279h10.018l3.096-9.5279-8.105-5.8885z" clip-rule="evenodd" fill="#276CFF" fill-rule="evenodd"/>
<path d="M172 78.1127L182.113 68L192.225 78.1127L182.113 88.2254L172 78.1127Z" fill="#00E567"/>
<path d="M161.889 43L152 57.3548H171.778L161.889 43Z" clip-rule="evenodd" fill="#FFC600" fill-rule="evenodd"/>
<path d="M20.8889 50L11 64.3548H30.7778L20.8889 50Z" clip-rule="evenodd" fill="#FFC600" fill-rule="evenodd"/>
<path d="m103.5 86.237698c21.7892 0 39.44261-17.70893 39.44261-39.553645 0-21.844824-17.65341-39.553755-39.44261-39.553755-21.789196 0-39.442611 17.708931-39.442611 39.553755 0 21.844715 17.653415 39.553645 39.442611 39.553645z" fill="#2683ff" stroke-width=".277569"/>
<path class="navigation-svg-path" d="m139.42219 82.605693-0.51394-13.874579-6.2921-0.125523-0.28995-6.456569-7.41032-1.243962 0.0294-6.136145-6.22569-0.05917-0.74758-6.913905-6.55937-6.558432 3.80223-14.191895c0.67195-2.507928-0.0449-5.183527-1.88093-7.019509l-15.13521-15.1349735c-1.8359-1.835982-4.511748-2.552892-7.019635-1.880946l-20.674955 5.5398834c-2.507844 0.671946-4.466742 2.6306351-5.138731 5.1385631l-5.539796 20.674854c-0.671989 2.507928 0.04497 5.183947 1.880904 7.019676l15.1351 15.1351c1.8359 1.835898 4.511752 2.552892 7.019629 1.880904l14.190724-3.800973 27.49467 27.493728zm-52.415063-52.415183c-3.121844 3.121882-8.183376 3.121882-11.305215 0-3.12184-3.121883-3.12184-8.183123 0-11.305006 3.121839-3.121882 8.183371-3.121882 11.305215 0 3.121843 3.121883 3.121843 8.183123 0 11.305006z" clip-rule="evenodd" fill="#dedefc" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,355 +0,0 @@
/* Copyright (C) 2019 Storj Labs, Inc. */
/* See LICENSE for copying information. */
body {
padding: 0 !important;
margin: 0 !important;
}
a {
text-decoration: none !important;
}
@font-face {
font-family: 'font_regular';
font-display: swap;
src: url('../fonts/font_regular.ttf');
}
@font-face {
font-family: 'font_bold';
font-display: swap;
src: url('../fonts/font_bold.ttf');
}
p {
font-family: 'font_medium', sans-serif;
font-size: 16px;
line-height: 21px;
color: #354049;
margin: 0 0 5px 0;
}
.reset-password-container {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 10;
background-size: contain;
display: flex;
justify-content: flex-start;
flex-direction: column;
align-items: flex-start;
padding: 60px 0 0 104px;
background-color: #f5f6fa;
}
.reset-password-container__wrapper {
min-width: 45%;
height: 86vh;
}
.reset-password-container__header {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
width: 100%;
}
.reset-password-container__logo {
cursor: pointer;
width: 140px;
height: 62px;
}
.reset-password-container__login-button {
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
border-radius: 6px;
border: 1px solid #afb7c1;
cursor: pointer;
width: 160px;
height: 48px;
}
.reset-password-area__submit-container__send-button {
display: flex;
align-items: center;
justify-content: center;
background-color: #2683ff;
border-radius: 6px;
cursor: pointer;
width: 100%;
height: 48px;
box-shadow: none;
}
.reset-password-area__submit-container__send-button p {
font-family: 'font_bold', sans-serif;
font-size: 14px;
line-height: 19px;
margin-block-start: 0;
margin-block-end: 0;
color: white;
}
.reset-password-area__submit-container__send-button:hover {
box-shadow: 0 4px 20px rgba(35, 121, 236, 0.4);
}
.reset-password-container__login-button p {
font-family: 'font_bold', sans-serif;
font-size: 14px;
line-height: 19px;
margin-block-start: 0;
margin-block-end: 0;
color: #354049;
}
.reset-password-container__login-button:hover {
background-color: #2683ff;
border: 1px solid #2683ff;
}
.reset-password-container__login-button:hover p {
color: white;
}
.reset-password-area-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: flex-start;
justify-content: flex-end;
margin-top: 50px;
}
.reset-password-area {
background-color: transparent;
width: 477px;
border-radius: 6px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: flex-start;
margin-top: 150px;
}
.reset-password-area__title-container {
height: 48px;
display: flex;
justify-content: space-between;
align-items: flex-end;
flex-direction: row;
margin-bottom: 20px;
width: 100%;
}
.reset-password-area__title-container h1 {
font-family: 'font_bold', sans-serif;
font-size: 22px;
color: #384b65;
line-height: 27px;
margin-block-start: 0;
margin-block-end: 0;
}
.reset-password-area__submit-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
border: none;
padding: 0;
}
.input-wrap {
position: relative;
width: 100%;
height: 46px;
margin-bottom: 20px;
}
input {
font-family: 'font_regular', sans-serif;
font-size: 16px;
line-height: 21px;
resize: none;
padding: 0;
height: 100%;
width: 100%;
text-indent: 20px;
border: 1px solid rgba(56, 75, 101, 0.4);
border-radius: 6px;
}
.eye-icon {
position: absolute;
right: 25px;
top: 50%;
transform: translate(0, -50%);
}
.reset-password-area input::placeholder {
color: #384b65;
opacity: 0.4;
}
.reset-password-area input:first-of-type {
margin-bottom: 15px;
}
.planet {
position: absolute;
bottom: -61px;
right: -257px;
z-index: -100;
}
.image {
position: fixed;
right: 0;
top: 0;
bottom: 0;
height: 100vh;
}
.st1 { fill: #2683ff; }
.st2 { fill: #007aff; }
.st3 { fill: #0062ff; }
.st4 { fill: #fff; }
.st5 { fill: #6cb9ff; }
.st6 { fill: #499ffc; }
.st7 { fill: #f7f9f9; }
.st8 { fill: #0f002d; }
.st9 { fill: #dedefc; }
@media screen and (max-width: 1500px) {
.reset-password-container__wrapper {
min-width: 45%;
}
}
@media screen and (max-width: 1300px) {
.reset-password-container__wrapper {
min-width: 75%;
}
.image {
display: none;
}
}
@media screen and (max-height: 840px) {
.reset-password-container {
overflow: hidden;
}
.reset-password-container__wrapper {
height: 770px;
overflow-y: scroll;
overflow-x: hidden;
-ms-overflow-style: none;
overflow: -moz-scrollbars-none;
}
.reset-password-container__wrapper::-webkit-scrollbar {
width: 0 !important;
display: none;
}
}
@media screen and (max-height: 810px) {
.reset-password-container__wrapper {
height: 700px;
}
.reset-password-area__submit-container {
margin-bottom: 25px;
}
}
@media screen and (max-width: 840px) {
.planet {
display: none;
}
}
@media screen and (max-width: 800px) {
.reset-password-container {
padding: 0;
align-items: center;
justify-content: center;
}
.planet {
display: none;
}
}
@media screen and (max-width: 650px) {
.reset-password-container__wrapper {
width: 500px;
}
}
@media screen and (max-width: 520px) {
.reset-password-container__wrapper {
width: 400px;
}
}
@media screen and (max-width: 420px) {
.reset-password-container__wrapper {
width: 350px;
}
}
@media screen and (max-width: 320px) {
.reset-password-container__login-button {
width: 120px;
height: 42px;
}
.reset-password-container__wrapper {
width: 300px;
}
.reset-password-container__header {
margin-top: 20px;
}
.reset-password-area__title-container h1 {
font-size: 20px;
line-height: 22px;
}
.reset-password-area__submit-container__terms-area h2 {
font-size: 12px;
}
.reset-password-area__submit-container__send-button {
height: 40px;
}
.reset-password-area__submit-container__send-button p {
font-size: 12px;
}
}

View File

@ -1,228 +0,0 @@
<!--Copyright (C) 2019 Storj Labs, Inc.-->
<!--See LICENSE for copying information.-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .SatelliteName }}</title>
<link rel="stylesheet" type="text/css" href="/static/static/resetPassword/resetPassword.css">
<script src="/static/static/resetPassword/resetPassword.js"></script>
</head>
<body>
<form name="resetPasswordForm" method="POST">
<div class="reset-password-container">
<svg class="image" alt="Authentication illustration" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 602 939.6" style="enable-background:new 0 0 602 939.6;" xml:space="preserve">
<rect x="67" y="1" class="st1" width="534.6" height="938.5"/>
<path class="st2" d="M67,939L520.5,71.5L584,87l101.8,368.9V939H163.5H67z"/>
<path class="st2" d="M429.3,470l-1.9-379.3c0-4.6-1.9-8.7-5-11.8c-3.1-3-7.2-5-11.8-5c-4.6,0-8.8,1.9-11.8,5c-3.1,3-5,7.2-5,11.8
c0,7.3-2.9,13.8-7.7,18.6c-4.8,4.8-11.4,7.7-18.6,7.7c-7.3,0-13.8-2.9-18.6-7.7c-4.8-4.8-7.7-11.4-7.7-18.6V42h9.6v48.7
c0,4.6,1.9,8.7,5,11.8c3.1,3,7.3,5,11.8,5c4.6,0,8.8-1.9,11.8-5c3.1-3,5-7.3,5-11.8c0-7.3,2.9-13.8,7.7-18.6
c4.8-4.8,11.4-7.7,18.6-7.7c7.3,0,13.8,2.9,18.6,7.7c4.8,4.8,7.7,11.4,7.7,18.6l1.9,379.3H429.3z"/>
<path class="st3" d="M580,637H462v302h118V637z"/>
<path class="st4" d="M586,708.5c86.1,0,155.9-69.9,155.9-156.3S672.1,396,586,396s-155.9,69.9-155.9,156.3S499.9,708.5,586,708.5z"/>
<path class="st5" d="M601.6,1H631v838h-51V1.2L601.6,1z"/>
<path class="st6" d="M553.1,707.6c-36.8-9.1-68.8-30.6-91.3-59.6V530.8c0-1.5,0.6-3.1,1.8-4.3l43.9-44.1h45.6v45.8V707.6z"/>
<path class="st4" d="M461.8,672.8h-85.3c-4.2,0-8-1.7-10.8-4.5c-2.8-2.7-4.5-6.5-4.5-10.7s1.7-8,4.5-10.7
c2.7-2.7,6.6-4.5,10.8-4.5c6.6,0,12.5-2.7,16.9-7c4.3-4.3,7-10.3,7-16.8c0-6.6-2.7-12.5-7-16.8c-4.3-4.3-10.3-7-16.9-7H67v9
c100.3,0,209.1-0.3,309.5-0.3c4.2,0,8,1.7,10.8,4.5c2.7,2.7,4.5,6.5,4.5,10.7s-1.7,8-4.5,10.7c-2.7,2.7-6.6,4.5-10.8,4.5
c-6.6,0-12.5,2.7-16.9,7c-4.3,4.3-7,10.3-7,16.8c0,6.6,2.7,12.5,7,16.8c4.3,4.3,10.3,7,16.9,7h85.3V672.8z"/>
<path class="st1" d="M581,712.2c-27.4-1.9-52.9-10.6-75-24.2V530.8c0-1.5,0.6-3.1,1.8-4.3l43.9-44.1H580L581,712.2z"/>
<path class="st1" d="M630,483h-50v18h50V483z"/>
<path class="st1" d="M630.5,525h-50.3v17.8h50.3V525z"/>
<path class="st1" d="M630.5,566.8h-50.3v18.6h50.3V566.8z"/>
<path class="st1" d="M630.5,609.4h-50.3v18.6h50.3V609.4z"/>
<path class="st1" d="M630.5,651.9h-50.3v17.8h50.3V651.9z"/>
<path class="st1" d="M630.5,693.7h-50.3v18.6h50.3V693.7z"/>
<path class="st3" d="M630.5,736.2h-51.8v18.6h51.8V736.2z"/>
<path class="st3" d="M630.5,778.8h-51.8v18.6h51.8V778.8z"/>
<path class="st3" d="M630.5,820.5h-51.8V940h51.8V820.5z"/>
<path class="st7" d="M471.9,535.9h-6.2V549h6.2V535.9z"/>
<path class="st7" d="M485,535.9h-7V549h7V535.9z"/>
<path class="st7" d="M497.4,535.9h-7V549h7V535.9z"/>
<path class="st7" d="M471.9,556h-6.2v13.9h6.2V556z"/>
<path class="st7" d="M485,556h-7v13.9h7V556z"/>
<path class="st7" d="M497.4,556h-7v13.9h7V556z"/>
<path class="st7" d="M471.9,576.9h-6.2V590h6.2V576.9z"/>
<path class="st7" d="M485,576.9h-7V590h7V576.9z"/>
<path class="st7" d="M497.4,576.9h-7V590h7V576.9z"/>
<path class="st7" d="M471.9,597h-6.2v13.2h6.2V597z"/>
<path class="st7" d="M485,597h-7v13.2h7V597z"/>
<path class="st7" d="M497.4,597h-7v13.2h7V597z"/>
<path class="st7" d="M471.9,617.1h-6.2v13.2h6.2V617.1z"/>
<path class="st7" d="M485,617.1h-7v13.2h7V617.1z"/>
<path class="st7" d="M497.4,617.1h-7v13.2h7V617.1z"/>
<path class="st7" d="M485,637.2h-7v13.1h7V637.2z"/>
<path class="st7" d="M497.4,637.2h-7v13.1h7V637.2z"/>
<path class="st8" d="M312.7,634.3h17.5c1.8,0,3.2-1.4,3.2-3.1c0-1.7-1.5-3.1-3.2-3.1h-17.5c-1.8,0-3.2,1.4-3.2,3.1
C309.4,632.9,310.9,634.3,312.7,634.3z"/>
<path class="st8" d="M312.6,621.9h30.7c1.8,0,3.2-1.5,3.2-3.5s-1.5-3.5-3.2-3.5h-30.6c-1.8,0-3.2,1.5-3.2,3.5
S310.8,621.9,312.6,621.9z"/>
<path class="st5" d="M355.2,621.9h2.1c1.8,0,3.2-1.5,3.2-3.5s-1.4-3.5-3.2-3.5h-2.1c-1.8,0-3.2,1.5-3.2,3.5
S353.4,621.9,355.2,621.9z"/>
<path class="st5" d="M367.6,621.9h2.1c1.8,0,3.2-1.5,3.2-3.5s-1.4-3.5-3.2-3.5h-2.1c-1.8,0-3.2,1.5-3.2,3.5
S365.8,621.9,367.6,621.9z"/>
<path class="st6" d="M350.2,18.4h-36.8v62.5h36.8V18.4z"/>
<path class="st8" d="M360.1,0h-56.6v32.9h56.6V0z"/>
<path class="st6" d="M362.7,88.1v4.7c0,16.6-13.5,30.2-29.9,30.2c-16.4,0-29.9-13.6-29.9-30.2v-4.7c0-16.6,13.5-30.2,29.9-30.2
C349.2,57.9,362.7,71.5,362.7,88.1z"/>
<path class="st6" d="M350.2,117.8v35.4c0,5.5-4.5,9.9-9.9,9.9c-5.5,0-9.9-4.5-9.9-9.9v-35.4c0-5.5,4.5-9.9,9.9-9.9
C345.7,107.8,350.2,112.4,350.2,117.8z"/>
<path class="st5" d="M321.9,109.8v9.5c0,4.7-3.8,8.4-8.6,8.4c-4.7,0-8.6-3.8-8.6-8.4v-9.5c0-4.7,3.8-8.4,8.6-8.4
C318.1,101.3,321.9,105.1,321.9,109.8z"/>
<path class="st5" d="M311.4,103v7.1c0,3.5-3,6.3-6.6,6.3c-3.6,0-6.6-2.8-6.6-6.3V103c0-3.5,3-6.3,6.6-6.3
C308.4,96.7,311.4,99.6,311.4,103z"/>
<path class="st5" d="M334.4,118v10.1c0,5.1-4.3,9.4-9.5,9.4c-5.3,0-9.5-4.2-9.5-9.4V118c0-5.2,4.3-9.4,9.5-9.4
C330.1,108.6,334.4,112.8,334.4,118z"/>
<path class="st5" d="M363.6,80.1l0.4,31.8c0.1,5.5-4.4,10.2-9.9,10.4c-5.5,0.2-10.1-4.1-10.1-9.7l-0.4-31.8
c-0.1-5.5,4.4-10.2,9.9-10.4C358.9,70.2,363.5,74.6,363.6,80.1z"/>
<path class="st5" d="M360.4,120.3l-6.7,8.4c-3.4,4.2-9.6,4.9-13.8,1.4c-4.1-3.5-4.7-9.8-1.3-14.1l6.7-8.4
c3.4-4.2,9.6-4.9,13.8-1.4C363.3,109.7,363.9,116.1,360.4,120.3z"/>
<path class="st5" d="M327.8,79.9c0,9.6,7.8,17.4,17.4,17.4c9.6,0,17.4-7.8,17.4-17.4c0-9.6-7.8-17.4-17.4-17.4
C335.6,62.5,327.8,70.3,327.8,79.9z"/>
<path class="st8" d="M334.2,175.9c-1.1-0.7-2.4,0.1-2.4,1.4v1.3v1.9c0,0.6,0.3,1.1,0.8,1.4l6.6,3.9c0.5,0.3,1.1,0.3,1.6,0l6.6-3.9
c0.5-0.3,0.8-0.9,0.8-1.4v-1.9v-1.3c0-1.3-1.4-2-2.4-1.4l-5,3c-0.5,0.3-1.1,0.3-1.6,0L334.2,175.9z"/>
<path class="st8" d="M334.2,188.4c-1.1-0.7-2.4,0.1-2.4,1.4v1.3v1.9c0,0.6,0.3,1.1,0.8,1.4l6.6,3.9c0.5,0.3,1.1,0.3,1.6,0l6.6-3.9
c0.5-0.3,0.8-0.9,0.8-1.4v-1.9v-1.3c0-1.3-1.4-2-2.4-1.4l-5,3c-0.5,0.3-1.1,0.3-1.6,0L334.2,188.4z"/>
<path class="st6" d="M580,0h-73v483h73V0z"/>
<path class="st4" d="M552.5,469.3H570c1.8,0,3.2-1.4,3.2-3.1s-1.5-3.1-3.2-3.1h-17.5c-1.8,0-3.2,1.4-3.2,3.1
S550.7,469.3,552.5,469.3z"/>
<path class="st4" d="M552.6,457h5c1.9,0,3.3-1.5,3.3-3.5c0-1.9-1.5-3.5-3.3-3.5h-5c-1.9,0-3.3,1.5-3.3,3.5
C549.3,455.4,550.7,457,552.6,457z"/>
<path class="st8" d="M552.6,444.6h5c1.9,0,3.3-1.4,3.3-3.1s-1.5-3.1-3.3-3.1h-5c-1.9,0-3.3,1.4-3.3,3.1S550.7,444.6,552.6,444.6z"/>
<path class="st9" d="M236.6,646.9c0.5-0.5,0.9-1.2,0.9-2v-32.8c0-1.5-1.2-2.8-2.8-2.8H227v-7.8c0-8-6.4-14.5-14.3-14.5
c-7.9,0-14.3,6.5-14.3,14.5v7.8h-7.7c-1.5,0-2.8,1.2-2.8,2.8v32.8c0,0.8,0.3,1.5,0.9,2l10.8,10.6c0.5,0.5,1.2,0.8,1.9,0.8h22.3
c0.7,0,1.4-0.3,1.9-0.8L236.6,646.9z M215.4,638.5v3.6c0,1.5-1.2,2.8-2.8,2.8c-1.5,0-2.8-1.2-2.8-2.8v-3.6c-3-1.2-5.2-4.1-5.2-7.6
c0-4.4,3.6-8,8-8c4.4,0,8,3.6,8,8C220.7,634.4,218.5,637.3,215.4,638.5z M221.6,609.3H204v-7.8c0-4.9,3.9-8.9,8.7-8.9
c4.8,0,8.7,4,8.7,8.9L221.6,609.3z"/>
<path class="st4" d="M156,568.3l-2.8-2.7c0.9-0.9,1.9-1.9,2.9-2.7l2.6,2.9C157.9,566.5,156.9,567.4,156,568.3z M161.7,563.2
l-2.5-2.9c1-0.9,2.1-1.6,3.2-2.4l2.3,3.1C163.7,561.6,162.7,562.4,161.7,563.2z M167.9,558.7l-2.2-3.2c1.1-0.7,2.2-1.4,3.4-2.1
l2,3.3C170,557.3,168.9,558,167.9,558.7z M251,556.6c-1-0.5-2-1.2-3-1.6l1.9-3.4c1.1,0.5,2.1,1.2,3.2,1.8L251,556.6z M174.4,554.8
l-1.9-3.4c1.2-0.6,2.4-1.2,3.6-1.8l1.7,3.5C176.7,553.7,175.5,554.3,174.4,554.8z M244.6,553.2c-1.2-0.5-2.3-1-3.5-1.5l1.5-3.6
c1.2,0.5,2.5,1,3.7,1.6L244.6,553.2z M181.3,551.7l-1.5-3.6c1.2-0.5,2.5-0.9,3.8-1.4l1.3,3.6C183.7,550.7,182.6,551.1,181.3,551.7
z M237.6,550.4c-1.2-0.4-2.4-0.8-3.6-1.2l1.2-3.7c1.3,0.4,2.6,0.8,3.8,1.2L237.6,550.4z M188.5,549.1l-1.2-3.7
c1.3-0.4,2.6-0.7,3.9-1l0.9,3.7C191,548.4,189.8,548.7,188.5,549.1z M230.2,548.1c-1.2-0.3-2.5-0.5-3.7-0.8l0.8-3.8
c1.3,0.2,2.6,0.5,3.9,0.9L230.2,548.1z M196,547.3l-0.8-3.8c1.3-0.2,2.6-0.5,3.9-0.6l0.5,3.8C198.5,546.8,197.3,547,196,547.3z
M222.7,546.6c-1.2-0.2-2.6-0.3-3.8-0.5l0.4-3.9c1.3,0.1,2.7,0.3,4,0.5L222.7,546.6z M203.6,546.2l-0.4-3.9c1.3-0.1,2.7-0.2,4-0.2
l0.2,3.9C206.2,545.9,204.9,546.1,203.6,546.2z M215.1,545.9c-1.2-0.1-2.6-0.1-3.9-0.1V542c1.3,0,2.7,0,4,0.1L215.1,545.9z"/>
<path class="st4" d="M212.7,704.5l-0.1-3.9c1.2,0,2.6-0.1,3.8-0.2l0.2,3.9C215.4,704.4,214,704.5,212.7,704.5z M208.6,704.4
c-1.3-0.1-2.7-0.2-4-0.2l0.3-3.9c1.2,0.2,2.6,0.2,3.8,0.2L208.6,704.4z M220.7,703.9l-0.5-3.9c1.2-0.2,2.6-0.3,3.8-0.5l0.6,3.9
C223.3,703.6,222,703.8,220.7,703.9z M200.7,703.8c-1.3-0.2-2.7-0.4-3.9-0.6l0.7-3.8c1.2,0.2,2.5,0.5,3.8,0.6L200.7,703.8z
M228.7,702.7l-0.9-3.8c1.2-0.3,2.5-0.6,3.7-0.9l1,3.8C231.2,702.1,229.9,702.4,228.7,702.7z M192.7,702.4c-1.3-0.3-2.6-0.6-3.9-1
l1.1-3.7c1.2,0.4,2.5,0.7,3.7,0.9L192.7,702.4z M236.4,700.6l-1.2-3.7c1.2-0.4,2.4-0.9,3.6-1.2l1.4,3.6
C239,699.7,237.7,700.1,236.4,700.6z M185,700.1c-1.2-0.5-2.6-0.9-3.8-1.4l1.5-3.6c1.2,0.5,2.4,0.9,3.6,1.3L185,700.1z
M243.9,697.7l-1.5-3.6c1.2-0.5,2.3-1.1,3.5-1.6l1.7,3.5C246.3,696.6,245.2,697.3,243.9,697.7z M177.5,697.3
c-1.2-0.5-2.4-1.2-3.6-1.8l1.8-3.5c1.2,0.6,2.2,1.2,3.5,1.6L177.5,697.3z M251.1,694.2l-1.9-3.4c1.1-0.6,2.2-1.2,3.3-1.9l2.1,3.3
C253.4,692.9,252.3,693.5,251.1,694.2z M170.4,693.5c-1.2-0.7-2.3-1.4-3.4-2.1l2.1-3.2c1.1,0.7,2.2,1.4,3.2,2L170.4,693.5z
M257.9,690l-2.2-3.2c1.1-0.7,2.1-1.5,3.1-2.2l2.4,3.1C260.1,688.5,259,689.3,257.9,690z M163.6,689.3c-1.1-0.8-2.2-1.6-3.2-2.4
l2.4-3c1,0.8,2,1.5,3.1,2.3L163.6,689.3z M264.3,685.2l-2.6-2.9c1-0.9,1.9-1.7,2.9-2.6l2.7,2.9
C266.3,683.4,265.3,684.3,264.3,685.2z M157.4,684.3c-0.6-0.5-1.2-1.1-1.8-1.6l2.7-2.9c0.5,0.5,1.2,1,1.7,1.5L157.4,684.3z
M270.1,679.7l-2.9-2.7c0.9-0.9,1.8-1.9,2.6-2.8l2.9,2.6C272,677.8,271.1,678.8,270.1,679.7z M275.4,673.7l-3.1-2.4
c0.8-1,1.5-2,2.3-3l3.2,2.2C277.1,671.6,276.3,672.7,275.4,673.7z M280.1,667.2l-3.3-2.1c0.7-1.1,1.4-2.2,2-3.2l3.4,1.9
C281.6,665,280.8,666.1,280.1,667.2z M284.1,660.3l-3.5-1.8c0.6-1.2,1.2-2.2,1.6-3.4l3.6,1.6C285.3,658,284.8,659.1,284.1,660.3z
M287.5,653l-3.6-1.5c0.5-1.2,0.9-2.4,1.3-3.6l3.7,1.2C288.4,650.5,287.9,651.8,287.5,653z M290,645.5l-3.8-1.1
c0.4-1.2,0.7-2.5,0.9-3.6l3.8,0.9C290.7,642.9,290.4,644.2,290,645.5z M291.9,637.7L288,637c0.2-1.2,0.5-2.5,0.6-3.7l3.9,0.5
C292.3,635.1,292.1,636.4,291.9,637.7z"/>
<path class="st4" d="M569.7,873.4c-1.9,0-3.7,0.1-5.5,0.3c-19.2-37.3-58-62.7-102.8-62.7c-40.8,0-76.5,21-97.1,52.9
c-6-1.6-12.5-2.5-19-2.5c-40.7,0-73.6,33-73.6,73.6c0,1.5,0.1,2.9,0.2,4.4h359.4c0.1-1.5,0.2-2.9,0.2-4.4
C631.3,901,603.7,873.4,569.7,873.4z"/>
<path class="st9" d="M261.4,917.8c-1.2,0-2.2-1.1-2.2-2.3c0.6-17.4,8-33.2,19.7-44.7c11.7-11.5,27.6-18.9,45.3-19.5
c1.2,0,2.3,1,2.3,2.2v19.8c0,1.2-0.9,2.2-2.2,2.2c-10.9,0.5-20.8,5.2-28.1,12.4c-7.3,7.2-12,16.9-12.5,27.8
c-0.1,1.2-1.1,2.2-2.2,2.2L261.4,917.8L261.4,917.8z"/>
<path class="st6" d="M325.3,872.1c4.5,0,8.1-3.6,8.1-8.1c0-4.5-3.6-8.1-8.1-8.1c-4.5,0-8.1,3.6-8.1,8.1
C317.2,868.5,320.8,872.1,325.3,872.1z"/>
<path class="st8" d="M322.2,917c2.3,0,4.3-1.9,4.3-4.3c0-2.3-1.9-4.3-4.3-4.3c-2.3,0-4.3,1.9-4.3,4.3
C317.9,915,319.9,917,322.2,917z"/>
<path class="st2" d="M424,908.2v14c0,0.5-0.5,0.9-0.9,0.9h-7.5c-0.5,0-0.9-0.5-0.9-0.9v-12.8c0-0.5,0.3-0.9,0.8-0.9
c1.5-0.3,5-0.9,7.5-1.3C423.5,907.2,424,907.6,424,908.2z M439.6,906.2c0.5,0,0.9,0.5,0.9,0.9v15.2c0,0.5-0.5,0.9-0.9,0.9h-7.5
c-0.5,0-0.9-0.5-0.9-0.9v-14.9c0-0.5,0.4-0.9,0.9-0.9C433.7,906.3,437.4,906.2,439.6,906.2z M457,908.2v14c0,0.5-0.5,0.9-0.9,0.9
h-7.5c-0.5,0-0.9-0.5-0.9-0.9v-14.8c0-0.5,0.5-1,1-0.9c1.7,0.2,5.1,0.5,7.4,0.8C456.6,907.4,457,907.8,457,908.2z M465.2,908.9
c2.6,0.5,5,1.2,7.5,1.9c0.4,0.2,0.7,0.5,0.7,0.9v10.5c0,0.5-0.5,0.9-0.9,0.9H465c-0.5,0-0.9-0.5-0.9-0.9v-12.5
C464.1,909.2,464.6,908.8,465.2,908.9z"/>
<path class="st9" d="M631.3,940.2v-10.1H416.2c-0.9,0-1.5,0.6-1.5,1.5v7.1c0,0.9,0.7,1.5,1.5,1.5H631.3z"/>
<path class="st4" d="M224.7,144.8c3.6,0,6.6-3.1,6.6-7c0-3.9-2.9-7-6.6-7s-6.6,3.1-6.6,7C218.2,141.7,221.1,144.8,224.7,144.8z"/>
<path class="st4" d="M244.1,144.8c3.6,0,6.6-3.1,6.6-7c0-3.9-2.9-7-6.6-7c-3.6,0-6.6,3.1-6.6,7
C237.5,141.7,240.4,144.8,244.1,144.8z"/>
<path class="st5" d="M224.7,122.4c3.6,0,6.6-2.9,6.6-6.6c0-3.6-2.9-6.6-6.6-6.6s-6.6,2.9-6.6,6.6
C218.2,119.4,221.1,122.4,224.7,122.4z"/>
<path class="st5" d="M244.1,122.4c3.6,0,6.6-2.9,6.6-6.6c0-3.6-2.9-6.6-6.6-6.6c-3.6,0-6.6,2.9-6.6,6.6
C237.5,119.4,240.4,122.4,244.1,122.4z"/>
<path class="st4" d="M205.4,99.2c3.6,0,6.6-2.9,6.6-6.6c0-3.6-2.9-6.6-6.6-6.6s-6.6,2.9-6.6,6.6C198.8,96.3,201.8,99.2,205.4,99.2z"/>
<path class="st4" d="M224.7,99.2c3.6,0,6.6-2.9,6.6-6.6c0-3.6-2.9-6.6-6.6-6.6s-6.6,2.9-6.6,6.6C218.2,96.3,221.1,99.2,224.7,99.2z"/>
<path class="st4" d="M244.1,99.2c3.6,0,6.6-2.9,6.6-6.6c0-3.6-2.9-6.6-6.6-6.6c-3.6,0-6.6,2.9-6.6,6.6
C237.5,96.3,240.4,99.2,244.1,99.2z"/>
<path class="st5" d="M472.3,202.1c6.6,0,12-5.6,12-12.4c0-6.8-5.4-12.4-12-12.4c-6.6,0-12,5.6-12,12.4
C460.2,196.5,465.7,202.1,472.3,202.1z"/>
<path class="st8" d="M471.9,195.1c2.9,0,5.4-2.4,5.4-5.4c0-2.9-2.4-5.4-5.4-5.4c-2.9,0-5.4,2.4-5.4,5.4
C466.5,192.7,468.9,195.1,471.9,195.1z"/>
<path class="st4" d="M118.5,808.2c0,3.6,3.1,6.6,7,6.6c3.9,0,7-2.9,7-6.6s-3.1-6.6-7-6.6C121.7,801.7,118.5,804.6,118.5,808.2z"/>
<path class="st4" d="M118.5,827.6c0,3.6,3.1,6.6,7,6.6c3.9,0,7-2.9,7-6.6c0-3.6-3.1-6.6-7-6.6C121.7,821,118.5,824,118.5,827.6z"/>
<path class="st5" d="M141,827.6c0,3.6,2.9,6.6,6.6,6.6c3.6,0,6.6-2.9,6.6-6.6c0-3.6-2.9-6.6-6.6-6.6C143.9,821,141,824,141,827.6z"/>
<path class="st6" d="M310.5,749.1c6.6,0,12-5.6,12-12.4c0-6.8-5.4-12.4-12-12.4c-6.6,0-12,5.6-12,12.4
C298.5,743.6,303.9,749.1,310.5,749.1z"/>
<path class="st2" d="M310.1,742.1c2.9,0,5.4-2.4,5.4-5.4c0-2.9-2.4-5.4-5.4-5.4c-2.9,0-5.4,2.4-5.4,5.4
C304.7,739.7,307.1,742.1,310.1,742.1z"/>
<rect x="191" y="595" class="st4" width="17" height="8.9"/>
<path class="st9" d="M186,215c-0.6,0-1.2,0-1.7,0.1c-1.1,0-2-0.5-2.6-1.5c-12.2-22.3-36-37.4-63.3-37.4c-24.8,0-46.6,12.4-59.6,31.3
c-0.6,0.9-1.8,1.4-2.9,1.2c-3.2-0.7-6.6-1.1-10.1-1.1c-25.4,0-46,20.5-46,45.7c0,0,0,0,0,0.1c0,1.5,1.2,2.7,2.7,2.7h218.8
c1.5,0,2.7-1.2,2.7-2.7v-0.1C224.3,232.1,207.2,215,186,215z"/>
</svg>
<div class="reset-password-container__wrapper">
<div class="reset-password-container__header">
<img class="reset-password-container__logo" src="/static/static/images/Logo.svg" alt="Company logo">
<a href="/login">
<div class="reset-password-container__login-button">
<p>Back to Login</p>
</div>
</a>
</div>
<div class="reset-password-area-wrapper">
<div class="reset-password-area">
<div class="reset-password-area__title-container">
<h1>Please enter your new password.</h1>
</div>
<p id="passwordLabel">New Password</p>
<div class="input-wrap">
<input
id="passwordInput"
oninput="resetPasswordError()"
type="password"
name="password"
placeholder="New Password">
<img
id="passwordInput_eyeIcon"
class="eye-icon"
src="/static/static/images/common/passwordHidden.svg"
alt="Eye icon"
onclick="togglePasswordVisibility()"
>
</div>
<p id="repeatPasswordLabel">Repeat Password</p>
<div class="input-wrap">
<input
id="repeatPasswordInput"
oninput="resetRepeatPasswordError()"
type="password"
name="passwordRepeat"
placeholder="Confirm Password">
<img
id="repeatPasswordInput_eyeIcon"
class="eye-icon"
src="/static/static/images/common/passwordHidden.svg"
alt="Eye icon"
onclick="toggleRepeatPasswordVisibility()"
>
</div>
<a href="javascript: submit()" class="reset-password-area__submit-container">
<div class="reset-password-area__submit-container__send-button">
<p>Set New Password</p>
</div>
</a>
</div>
</div>
</div>
</div>
</form>
</body>
</html>

View File

@ -1,132 +0,0 @@
/* Copyright (C) 2019 Storj Labs, Inc. */
/* See LICENSE for copying information. */
const passwordType = 'password';
const textType = 'text';
const regularPasswordLabel = 'New Password';
const regularRepeatPasswordLabel = 'Repeat Password';
const errorPasswordLabel = 'Password must contains at least 6 characters';
const errorRepeatPasswordLabel = 'Password doesn\'t match';
const regularColor = '#354049';
const errorColor = '#eb5757';
const passwordInputId = 'passwordInput';
const repeatPasswordId = 'repeatPasswordInput';
const types = {
PASSWORD: 'PASSWORD',
REPEAT_PASSWORD: 'REPEAT_PASSWORD',
};
function togglePasswordVisibility() {
toggleVisibility(passwordInputId);
}
function toggleRepeatPasswordVisibility() {
toggleVisibility(repeatPasswordId);
}
function toggleVisibility(id) {
const element = document.getElementById(id);
if (element) {
const isPasswordVisible = element.type === textType;
toggleEyeIcon(id, isPasswordVisible);
element.type = isPasswordVisible ? passwordType : textType;
}
}
function toggleEyeIcon(id, isPasswordVisible) {
const eyeIcon = document.getElementById(`${id}_eyeIcon`);
if (eyeIcon) {
eyeIcon.src = isPasswordVisible ?
'/static/static/images/common/passwordHidden.svg' :
'/static/static/images/common/passwordShown.svg'
}
}
function submit() {
const passwordInput = document.getElementById(passwordInputId);
if (passwordInput) {
if (!validatePassword(passwordInput.value)) {
setPasswordError(true);
return;
}
}
const repeatPasswordInput = document.getElementById(repeatPasswordId);
if (repeatPasswordInput) {
if (passwordInput.value !== repeatPasswordInput.value) {
setRepeatPasswordError(true);
return;
}
}
document.resetPasswordForm.submit();
}
function setPasswordError(status) {
setError(types.PASSWORD, status);
}
function setRepeatPasswordError(status) {
setError(types.REPEAT_PASSWORD, status);
}
function setError(type, status) {
let passwordLabel;
let passwordInput;
let regularLabel;
let errorLabel;
switch (type) {
case types.PASSWORD:
passwordLabel = document.getElementById('passwordLabel');
passwordInput = document.getElementById(passwordInputId);
regularLabel = regularPasswordLabel;
errorLabel = errorPasswordLabel;
break;
case types.REPEAT_PASSWORD:
passwordLabel = document.getElementById('repeatPasswordLabel');
passwordInput = document.getElementById(repeatPasswordId);
regularLabel = regularRepeatPasswordLabel;
errorLabel = errorRepeatPasswordLabel;
}
if (passwordLabel && passwordInput) {
changeStyling({
labelElement: passwordLabel,
inputElement: passwordInput,
regularLabel,
errorLabel,
status,
});
}
}
function changeStyling(configuration) {
if (configuration.status) {
configuration.labelElement.innerText = configuration.errorLabel;
configuration.labelElement.style.color = configuration.inputElement.style.borderColor = errorColor;
return;
}
configuration.labelElement.innerText = configuration.regularLabel;
configuration.labelElement.style.color = configuration.inputElement.style.borderColor = regularColor;
}
function validatePassword(password) {
return typeof password !== 'undefined' && password.length >= 6;
}
function resetPasswordError() {
setError(types.PASSWORD, false);
}
function resetRepeatPasswordError() {
setError(types.REPEAT_PASSWORD, false);
}

View File

@ -1,71 +0,0 @@
/* Copyright (C) 2019 Storj Labs, Inc. */
/* See LICENSE for copying information. */
@font-face {
font-family: 'font_regular';
font-display: swap;
src: url('../fonts/font_regular.ttf') format('truetype');
}
.container {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
width: 100%;
height: 100vh;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
a {
text-decoration: none;
}
.container__title {
font-family: 'font_regular', sans-serif;
font-weight: bold;
font-size: 32px;
line-height: 39px;
}
.container__info {
width: 566px;
text-align: center;
font-family: 'font_regular', sans-serif;
font-size: 18px;
line-height: 26px;
}
.container__button {
display: flex;
align-items: center;
justify-content: center;
background-color: #2683ff;
border-radius: 6px;
cursor: pointer;
width: 216px;
height: 48px;
margin-top: 30px;
}
.container__button p {
font-family: 'font_regular', sans-serif;
font-weight: bold;
font-size: 16px;
line-height: 23px;
color: #fff;
}
.container__button:hover {
box-shadow: 0 4px 20px rgba(35, 121, 236, 0.4);
}

File diff suppressed because one or more lines are too long