Implemented password reset on satellite console web. (#1665)
This commit is contained in:
parent
db2f4615fd
commit
6a50b187eb
@ -6,6 +6,8 @@ package consoleql
|
||||
const (
|
||||
// ActivationPath is key for path which handles account activation
|
||||
ActivationPath = "activationPath"
|
||||
// PasswordRecoveryPath is key for path which handles password recovery
|
||||
PasswordRecoveryPath = "passwordRecoveryPath"
|
||||
// SignInPath is key for sign in server route
|
||||
SignInPath = "signInPath"
|
||||
)
|
||||
@ -34,7 +36,7 @@ type ForgotPasswordEmail struct {
|
||||
func (*ForgotPasswordEmail) Template() string { return "Forgot" }
|
||||
|
||||
// Subject gets email subject
|
||||
func (*ForgotPasswordEmail) Subject() string { return "" }
|
||||
func (*ForgotPasswordEmail) Subject() string { return "Password recovery request" }
|
||||
|
||||
// ProjectInvitationEmail is mailservice template for project invitation email
|
||||
type ProjectInvitationEmail struct {
|
||||
|
@ -4,10 +4,15 @@
|
||||
package consoleql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/graphql-go/graphql"
|
||||
"github.com/skyrings/skyring-common/tools/uuid"
|
||||
|
||||
"storj.io/storj/internal/post"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/mailservice"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -21,10 +26,12 @@ const (
|
||||
MyProjectsQuery = "myProjects"
|
||||
// TokenQuery is a query name for token
|
||||
TokenQuery = "token"
|
||||
// ForgotPasswordQuery is a query name for password recovery request
|
||||
ForgotPasswordQuery = "forgotPassword"
|
||||
)
|
||||
|
||||
// rootQuery creates query for graphql populated by AccountsClient
|
||||
func rootQuery(service *console.Service, types *TypeCreator) *graphql.Object {
|
||||
func rootQuery(service *console.Service, mailService *mailservice.Service, types *TypeCreator) *graphql.Object {
|
||||
return graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: Query,
|
||||
Fields: graphql.Fields{
|
||||
@ -90,6 +97,50 @@ func rootQuery(service *console.Service, types *TypeCreator) *graphql.Object {
|
||||
return tokenWrapper{Token: token}, nil
|
||||
},
|
||||
},
|
||||
ForgotPasswordQuery: &graphql.Field{
|
||||
Type: graphql.Boolean,
|
||||
Args: graphql.FieldConfigArgument{
|
||||
FieldEmail: &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(graphql.String),
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
email, _ := p.Args[FieldEmail].(string)
|
||||
|
||||
user, err := service.GetUserByEmail(p.Context, email)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%s is not found", email)
|
||||
}
|
||||
|
||||
recoveryToken, err := service.GeneratePasswordRecoveryToken(p.Context, user.ID, user.Email)
|
||||
if err != nil {
|
||||
return false, errors.New("failed to generate password recovery token")
|
||||
}
|
||||
|
||||
rootObject := p.Info.RootValue.(map[string]interface{})
|
||||
origin := rootObject["origin"].(string)
|
||||
link := origin + rootObject[PasswordRecoveryPath].(string) + recoveryToken
|
||||
userName := user.ShortName
|
||||
if user.ShortName == "" {
|
||||
userName = user.FullName
|
||||
}
|
||||
|
||||
// TODO: think of a better solution
|
||||
go func() {
|
||||
_ = mailService.SendRendered(
|
||||
p.Context,
|
||||
[]post.Address{{Address: user.Email, Name: userName}},
|
||||
&ForgotPasswordEmail{
|
||||
Origin: origin,
|
||||
ResetLink: link,
|
||||
UserName: userName,
|
||||
},
|
||||
)
|
||||
}()
|
||||
|
||||
return true, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -466,5 +466,51 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, rootUser.CreatedAt.Equal(createdAt))
|
||||
})
|
||||
|
||||
t.Run("PasswordReset query", func(t *testing.T) {
|
||||
regToken, err := service.CreateRegToken(ctx, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user, err := service.CreateUser(authCtx, console.CreateUser{
|
||||
UserInfo: console.UserInfo{
|
||||
FullName: "Example User",
|
||||
ShortName: "Example",
|
||||
Email: "user@example.com",
|
||||
},
|
||||
Password: "123a123",
|
||||
}, regToken.Secret)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("Activation", func(t *testing.T) {
|
||||
activationToken, err := service.GenerateActivationToken(
|
||||
ctx,
|
||||
user.ID,
|
||||
"user@example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = service.ActivateAccount(ctx, activationToken)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user.Email = "user@example.com"
|
||||
|
||||
})
|
||||
|
||||
rootObject[consoleql.PasswordRecoveryPath] = "?activationToken="
|
||||
query := fmt.Sprintf("query {forgotPassword(email: \"%s\")}", user.Email)
|
||||
|
||||
result := testQuery(t, query)
|
||||
assert.NotNil(t, result)
|
||||
data := result.(map[string]interface{})
|
||||
|
||||
ok := data[consoleql.ForgotPasswordQuery].(bool)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ func (c *TypeCreator) Create(log *zap.Logger, service *console.Service, mailServ
|
||||
}
|
||||
|
||||
// root objects
|
||||
c.query = rootQuery(service, c)
|
||||
c.query = rootQuery(service, mailService, c)
|
||||
if err := c.query.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -93,6 +93,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
|
||||
|
||||
if server.config.StaticDir != "" {
|
||||
mux.Handle("/activation/", http.HandlerFunc(server.accountActivationHandler))
|
||||
mux.Handle("/password-recovery/", http.HandlerFunc(server.passwordRecoveryHandler))
|
||||
mux.Handle("/registrationToken/", http.HandlerFunc(server.createRegistrationTokenHandler))
|
||||
mux.Handle("/usage-report/", http.HandlerFunc(server.bucketUsageReportHandler))
|
||||
mux.Handle("/static/", http.StripPrefix("/static", fs))
|
||||
@ -224,13 +225,55 @@ func (s *Server) accountActivationHandler(w http.ResponseWriter, req *http.Reque
|
||||
zap.String("token", activationToken),
|
||||
zap.Error(err))
|
||||
|
||||
http.ServeFile(w, req, filepath.Join(s.config.StaticDir, "static", "errors", "404.html"))
|
||||
s.serveError(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeFile(w, req, filepath.Join(s.config.StaticDir, "static", "activation", "success.html"))
|
||||
}
|
||||
|
||||
func (s *Server) passwordRecoveryHandler(w http.ResponseWriter, req *http.Request) {
|
||||
recoveryToken := req.URL.Query().Get("token")
|
||||
if len(recoveryToken) == 0 {
|
||||
s.serveError(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "POST":
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
s.serveError(w, req)
|
||||
}
|
||||
|
||||
password := req.FormValue("password")
|
||||
passwordRepeat := req.FormValue("passwordRepeat")
|
||||
if strings.Compare(password, passwordRepeat) != 0 {
|
||||
s.serveError(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.service.ResetPassword(context.Background(), recoveryToken, password)
|
||||
if err != nil {
|
||||
s.serveError(w, req)
|
||||
}
|
||||
default:
|
||||
t, err := template.ParseFiles(filepath.Join(s.config.StaticDir, "static", "resetPassword", "resetPassword.html"))
|
||||
if err != nil {
|
||||
s.serveError(w, req)
|
||||
}
|
||||
|
||||
err = t.Execute(w, nil)
|
||||
if err != nil {
|
||||
s.serveError(w, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) serveError(w http.ResponseWriter, req *http.Request) {
|
||||
http.ServeFile(w, req, filepath.Join(s.config.StaticDir, "static", "errors", "404.html"))
|
||||
}
|
||||
|
||||
// grapqlHandler is graphql endpoint http handler function
|
||||
func (s *Server) grapqlHandler(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set(contentType, applicationJSON)
|
||||
@ -254,6 +297,7 @@ func (s *Server) grapqlHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
rootObject["origin"] = s.config.ExternalAddress
|
||||
rootObject[consoleql.ActivationPath] = "activation/?token="
|
||||
rootObject[consoleql.PasswordRecoveryPath] = "password-recovery/?token="
|
||||
rootObject[consoleql.SignInPath] = "login"
|
||||
|
||||
result := graphql.Do(graphql.Params{
|
||||
|
@ -35,15 +35,16 @@ const (
|
||||
|
||||
// Error messages
|
||||
const (
|
||||
internalErrMsg = "It looks like we had a problem on our end. Please try again"
|
||||
unauthorizedErrMsg = "You are not authorized to perform this action"
|
||||
vanguardRegTokenErrMsg = "We are unable to create your account. This is an invite-only alpha, please join our waitlist to receive an invitation"
|
||||
emailUsedErrMsg = "This email is already in use, try another"
|
||||
activationTokenIsExpiredErrMsg = "Your account activation link has expired, please sign up again"
|
||||
credentialsErrMsg = "Your email or password was incorrect, please try again"
|
||||
oldPassIncorrectErrMsg = "Old password is incorrect, please try again"
|
||||
passwordIncorrectErrMsg = "Your password needs at least %d characters long"
|
||||
teamMemberDoesNotExistErrMsg = `There is no account on this Satellite for the user(s) you have entered.
|
||||
internalErrMsg = "It looks like we had a problem on our end. Please try again"
|
||||
unauthorizedErrMsg = "You are not authorized to perform this action"
|
||||
vanguardRegTokenErrMsg = "We are unable to create your account. This is an invite-only alpha, please join our waitlist to receive an invitation"
|
||||
emailUsedErrMsg = "This email is already in use, try another"
|
||||
activationTokenIsExpiredErrMsg = "Your account activation link has expired, please sign up again"
|
||||
passwordRecoveryTokenIsExpiredErrMsg = "Your password recovery link has expired, please request another one"
|
||||
credentialsErrMsg = "Your email or password was incorrect, please try again"
|
||||
oldPassIncorrectErrMsg = "Old password is incorrect, please try again"
|
||||
passwordIncorrectErrMsg = "Your password needs at least %d characters long"
|
||||
teamMemberDoesNotExistErrMsg = `There is no account on this Satellite for the user(s) you have entered.
|
||||
Please add team members with active accounts`
|
||||
|
||||
// TODO: remove after vanguard release
|
||||
@ -149,6 +150,19 @@ func (s *Service) GenerateActivationToken(ctx context.Context, id uuid.UUID, ema
|
||||
return s.createToken(claims)
|
||||
}
|
||||
|
||||
// GeneratePasswordRecoveryToken - is a method for generating password recovery token
|
||||
func (s *Service) GeneratePasswordRecoveryToken(ctx context.Context, id uuid.UUID, email string) (token string, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
claims := &consoleauth.Claims{
|
||||
ID: id,
|
||||
Email: email,
|
||||
Expiration: time.Now().Add(time.Hour),
|
||||
}
|
||||
|
||||
return s.createToken(claims)
|
||||
}
|
||||
|
||||
// ActivateAccount - is a method for activating user account after registration
|
||||
func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
@ -193,6 +207,42 @@ func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword - is a method for reseting user password
|
||||
func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, password string) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
token, err := consoleauth.FromBase64URLString(resetPasswordToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := s.authenticate(token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.store.Users().Get(ctx, claims.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := validatePassword(password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if time.Since(claims.Expiration) > 0 {
|
||||
return errs.New(passwordRecoveryTokenIsExpiredErrMsg)
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), s.passwordCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.PasswordHash = hash
|
||||
return s.store.Users().Update(ctx, user)
|
||||
}
|
||||
|
||||
// Token authenticates User by credentials and returns auth token
|
||||
func (s *Service) Token(ctx context.Context, email, password string) (token string, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
@ -238,6 +288,13 @@ func (s *Service) GetUser(ctx context.Context, id uuid.UUID) (u *User, err error
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail returns User by email
|
||||
func (s *Service) GetUserByEmail(ctx context.Context, email string) (u *User, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
return s.store.Users().GetByEmail(ctx, email)
|
||||
}
|
||||
|
||||
// UpdateAccount updates User
|
||||
func (s *Service) UpdateAccount(ctx context.Context, info UserInfo) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
@ -84,6 +84,33 @@ export async function changePasswordRequest(password: string, newPassword: strin
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function forgotPasswordRequest(email: string): Promise<RequestResponse<null>> {
|
||||
let result: RequestResponse<null> = {
|
||||
errorMessage: '',
|
||||
isSuccess: false,
|
||||
data: null
|
||||
};
|
||||
|
||||
let response: any = await apolloManager.query(
|
||||
{
|
||||
query: gql(`
|
||||
query {
|
||||
forgotPassword(email: "${email}")
|
||||
}`),
|
||||
fetchPolicy: 'no-cache',
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
);
|
||||
|
||||
if (response.errors) {
|
||||
result.errorMessage = response.errors[0].message;
|
||||
} else {
|
||||
result.isSuccess = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Performs Create user graqhQL request.
|
||||
export async function createUserRequest(user: User, password: string, secret: string): Promise<RequestResponse<null>> {
|
||||
let result: RequestResponse<null> = {
|
||||
|
@ -6,9 +6,9 @@
|
||||
<div class="team-header">
|
||||
<HeaderArea/>
|
||||
</div>
|
||||
<div id="scrollable_team_container" v-if="projectMembers.length > 0" v-on:scroll="handleScroll" class="team-container">
|
||||
<div id="scrollable_team_container" v-if="projectMembers.length > 0 || projectMembersCount > 0" v-on:scroll="handleScroll" class="team-container">
|
||||
<div class="team-container__content">
|
||||
<div v-for="(member, index) in projectMembers" v-on:click="onMemberClick(member)" v-bind:key="index">
|
||||
<div v-for="member in projectMembers" v-on:click="onMemberClick(member)" v-bind:key="member.id">
|
||||
<TeamMemberItem
|
||||
:projectMember = "member"
|
||||
v-bind:class = "[member.isSelected ? 'selected' : '']"
|
||||
@ -20,7 +20,7 @@
|
||||
<Footer/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-search-result-area" v-if="projectMembers.length === 0">
|
||||
<div class="empty-search-result-area" v-if="(projectMembers.length === 0 && projectMembersCount === 0)">
|
||||
<h1 class="empty-search-result-area__text">No results found</h1>
|
||||
<div class="empty-search-result-area__image" v-html="emptyImage"></div>
|
||||
</div>
|
||||
@ -71,6 +71,9 @@ import { NOTIFICATION_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames'
|
||||
projectMembers: function () {
|
||||
return this.$store.getters.projectMembers;
|
||||
},
|
||||
projectMembersCount: function () {
|
||||
return this.$store.getters.projectMembersCountGetter;
|
||||
},
|
||||
selectedProjectMembers: function () {
|
||||
return this.$store.getters.selectedProjectMembers;
|
||||
},
|
||||
|
@ -12,6 +12,7 @@ import { ProjectMemberSortByEnum } from '@/utils/constants/ProjectMemberSortEnum
|
||||
export const projectMembersModule = {
|
||||
state: {
|
||||
projectMembers: [],
|
||||
projectMembersCount: 0,
|
||||
searchParameters: {
|
||||
sortBy: ProjectMemberSortByEnum.NAME,
|
||||
searchQuery: ''
|
||||
@ -48,7 +49,11 @@ export const projectMembersModule = {
|
||||
});
|
||||
},
|
||||
[PROJECT_MEMBER_MUTATIONS.FETCH](state: any, teamMembers: any[]) {
|
||||
state.projectMembers = state.projectMembers.concat(teamMembers);
|
||||
teamMembers.forEach(value => {
|
||||
state.projectMembers.push(value);
|
||||
|
||||
});
|
||||
state.projectMembersCount = state.projectMembers.length;
|
||||
},
|
||||
[PROJECT_MEMBER_MUTATIONS.CLEAR](state: any) {
|
||||
state.projectMembers = [];
|
||||
@ -124,6 +129,7 @@ export const projectMembersModule = {
|
||||
},
|
||||
getters: {
|
||||
projectMembers: (state: any) => state.projectMembers,
|
||||
projectMembersCountGetter: (state: any) => state.projectMembersCount,
|
||||
selectedProjectMembers: (state: any) => state.projectMembers.filter((member: any) => member.isSelected),
|
||||
},
|
||||
};
|
||||
|
@ -7,17 +7,37 @@
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import HeaderlessInput from '../../components/common/HeaderlessInput.vue';
|
||||
import { LOADING_CLASSES } from '@/utils/constants/classConstants';
|
||||
import { forgotPasswordRequest } from '@/api/users';
|
||||
import { NOTIFICATION_ACTIONS } from '@/utils/constants/actionNames';
|
||||
|
||||
@Component(
|
||||
{
|
||||
data: function () {
|
||||
return {
|
||||
loadingClassName: LOADING_CLASSES.LOADING_OVERLAY,
|
||||
email: '',
|
||||
};
|
||||
},
|
||||
components: {
|
||||
HeaderlessInput,
|
||||
},
|
||||
methods: {
|
||||
setEmail: function (value: string): void {
|
||||
this.$data.email = value;
|
||||
},
|
||||
onSendConfigurations: async function (): Promise<any> {
|
||||
if (!this.$data.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
let passwordRecoveryResponse = await forgotPasswordRequest(this.$data.email);
|
||||
if (passwordRecoveryResponse.isSuccess) {
|
||||
this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Please look for instructions at your email');
|
||||
} else {
|
||||
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, passwordRecoveryResponse.errorMessage);
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default class ForgotPassword extends Vue {
|
||||
|
@ -27,7 +27,7 @@
|
||||
height="46px"
|
||||
isWhite>
|
||||
</HeaderlessInput>
|
||||
<div class="forgot-password-area__submit-container">
|
||||
<div class="forgot-password-area__submit-container" v-on:click.prevent="onSendConfigurations">
|
||||
<div class="forgot-password-area__submit-container__send-button" >
|
||||
<p>Send Configurations</p>
|
||||
</div>
|
||||
|
@ -22,6 +22,7 @@ import { AppState } from '@/utils/constants/appStateEnum';
|
||||
password: '',
|
||||
loadingClassName: LOADING_CLASSES.LOADING_OVERLAY,
|
||||
loadingLogoClassName: LOADING_CLASSES.LOADING_LOGO,
|
||||
forgotPasswordRouterPath: ROUTES.FORGOT_PASSWORD.path,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -33,7 +33,7 @@
|
||||
isPassword>
|
||||
</HeaderlessInput>
|
||||
<div class="login-area__submit-area">
|
||||
<router-link to="" class="login-area__navigation-area__nav-link" exact>
|
||||
<router-link v-bind:to="forgotPasswordRouterPath" class="login-area__navigation-area__nav-link" exact>
|
||||
<h3><strong>Forgot password?</strong></h3>
|
||||
</router-link>
|
||||
<div class="login-area__submit-area__login-button" v-on:click.prevent="onLogin">
|
||||
|
@ -512,7 +512,7 @@ To reset your password, click the following link and follow the instructions. &#
|
||||
|
||||
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 12px;Margin-bottom: 12px;">
|
||||
<div class="btn btn--flat btn--large" style="text-align:left;">
|
||||
<![if !mso]><a style="border-radius: 4px;display: inline-block;font-size: 14px;font-weight: bold;line-height: 24px;padding: 12px 24px;text-align: center;text-decoration: none !important;transition: opacity 0.1s ease-in;color: #ffffff !important;background-color: #2683ff;font-family: Montserrat, DejaVu Sans, Verdana, sans-serif;" href="{{ .ResetLink }}">Reset Password</a><![endif]>
|
||||
<a style="border-radius: 4px;display: inline-block;font-size: 14px;font-weight: bold;line-height: 24px;padding: 12px 24px;text-align: center;text-decoration: none !important;transition: opacity 0.1s ease-in;color: #ffffff !important;background-color: #2683ff;font-family: Montserrat, DejaVu Sans, Verdana, sans-serif;" href="{{ .ResetLink }}">Reset Password</a>
|
||||
<!--[if mso]><p style="line-height:0;margin:0;"> </p><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="{{ .ResetLink }}" style="width:191px" arcsize="9%" fillcolor="#2683FF" stroke="f"><v:textbox style="mso-fit-shape-to-text:t" inset="0px,11px,0px,11px"><center style="font-size:14px;line-height:24px;color:#FFFFFF;font-family:Montserrat,DejaVu Sans,Verdana,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:4px">Reset Password</center></v:textbox></v:roundrect><![endif]--></div>
|
||||
</div>
|
||||
|
||||
|
@ -113,6 +113,8 @@ body {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.reset-password-area__submit-container__send-button {
|
||||
display: flex;
|
||||
|
@ -4,43 +4,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tardigrade Satellite - Mars</title>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tardigrade Satellite - Mars</title>
|
||||
|
||||
<link href="/static/static/fonts/font_regular.ttf" rel="stylesheet">
|
||||
<link href="/static/static/fonts/font_bold.ttf" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="resetPassword.css">
|
||||
<link href="/static/static/fonts/font_regular.ttf" rel="stylesheet">
|
||||
<link href="/static/static/fonts/font_bold.ttf" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="/static/static/resetPassword/resetPassword.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="reset-password-container">
|
||||
<img class="image" src="../images/AuthImage.svg" alt="" >
|
||||
<div class="reset-password-container__wrapper">
|
||||
<div class="reset-password-container__header">
|
||||
<img class="reset-password-container__logo" src="../images/Logo.svg" alt="logo">
|
||||
<div class="reset-password-container__login-button" >
|
||||
<p>Back to Login</p>
|
||||
</div>
|
||||
</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>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="New Password">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Confirm Password">
|
||||
<div class="reset-password-area__submit-container">
|
||||
<div class="reset-password-area__submit-container__send-button" >
|
||||
<p>Set New Password</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<div class="reset-password-container">
|
||||
<img class="image" src="/static/static/images/AuthImage.svg" alt="">
|
||||
<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="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>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="New Password">
|
||||
<input
|
||||
type="password"
|
||||
name="passwordRepeat"
|
||||
placeholder="Confirm Password">
|
||||
<button type="submit" class="reset-password-area__submit-container">
|
||||
<div class="reset-password-area__submit-container__send-button">
|
||||
<p>Set New Password</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user