Compare commits
7 commits
25630bba41
...
6dcb5a4963
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dcb5a4963 | |||
| cbc27f1706 | |||
| d4358479f6 | |||
| 0f7176999b | |||
| 0d604ef320 | |||
| e64f5dbabf | |||
| e6bfa62381 |
18 changed files with 113 additions and 287 deletions
|
|
@ -99,14 +99,13 @@ class FFTCGDB
|
||||||
stmt.finalize()
|
stmt.finalize()
|
||||||
if err
|
if err
|
||||||
logger.warn "reg: FAIL db '#{err.code}' for '#{login}'"
|
logger.warn "reg: FAIL db '#{err.code}' for '#{login}'"
|
||||||
reject messages.exists # user already exists
|
# user already exists
|
||||||
|
reject messages.exists
|
||||||
|
|
||||||
else
|
else
|
||||||
logger.info "reg: OK '#{login}'"
|
logger.info "reg: OK '#{login}'"
|
||||||
# registration successful
|
# registration successful
|
||||||
resolve
|
resolve null
|
||||||
user: @lastID
|
|
||||||
login: login
|
|
||||||
|
|
||||||
.catch ->
|
.catch ->
|
||||||
reject messages.empty
|
reject messages.empty
|
||||||
|
|
@ -117,7 +116,7 @@ class FFTCGDB
|
||||||
@validate login, password
|
@validate login, password
|
||||||
.then =>
|
.then =>
|
||||||
# get users table row
|
# get users table row
|
||||||
stmt = @db.prepare 'SELECT user, login, pwdhash FROM users WHERE login = ?'
|
stmt = @db.prepare 'SELECT * FROM users WHERE login = ?'
|
||||||
stmt.get [login], (err, row) =>
|
stmt.get [login], (err, row) =>
|
||||||
stmt.finalize()
|
stmt.finalize()
|
||||||
if err
|
if err
|
||||||
|
|
@ -139,9 +138,7 @@ class FFTCGDB
|
||||||
if res == true
|
if res == true
|
||||||
logger.debug "login: OK '#{row.login}'"
|
logger.debug "login: OK '#{row.login}'"
|
||||||
# login successful
|
# login successful
|
||||||
resolve
|
resolve row.user
|
||||||
user: row.user
|
|
||||||
login: row.login
|
|
||||||
|
|
||||||
else
|
else
|
||||||
logger.debug "login: FAIL password for '#{login}'"
|
logger.debug "login: FAIL password for '#{login}'"
|
||||||
|
|
@ -150,6 +147,26 @@ class FFTCGDB
|
||||||
.catch ->
|
.catch ->
|
||||||
reject messages.empty
|
reject messages.empty
|
||||||
|
|
||||||
|
get: (user) ->
|
||||||
|
new Promise (resolve, reject) =>
|
||||||
|
# get users table row
|
||||||
|
stmt = @db.prepare 'SELECT * FROM users WHERE user = ?'
|
||||||
|
stmt.get [user], (err, row) =>
|
||||||
|
stmt.finalize()
|
||||||
|
if err
|
||||||
|
logger.warn "get: FAIL db '#{err.code}' for '#{user}'"
|
||||||
|
reject messages.db
|
||||||
|
|
||||||
|
else if not row
|
||||||
|
logger.debug "get: FAIL nonexistent '#{user}'"
|
||||||
|
reject messages.noexists # user doesnt exist
|
||||||
|
|
||||||
|
else
|
||||||
|
resolve
|
||||||
|
user: row.user
|
||||||
|
login: row.login
|
||||||
|
settings: row.settings
|
||||||
|
|
||||||
addDeck: (user, deckCards) ->
|
addDeck: (user, deckCards) ->
|
||||||
new Promise (resolve, reject) =>
|
new Promise (resolve, reject) =>
|
||||||
# try creating row in decks table
|
# try creating row in decks table
|
||||||
|
|
|
||||||
34
backend/routes/user/info.coffee
Normal file
34
backend/routes/user/info.coffee
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
logger = (require 'logging').default '/user/info'
|
||||||
|
|
||||||
|
# session storage (volatile data)
|
||||||
|
session = (require '../../session')
|
||||||
|
# fftcg.db (persistent data)
|
||||||
|
fftcgdb = (require '../../db')
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
url: '/user/info'
|
||||||
|
method: 'POST'
|
||||||
|
schema: (require './info.schema')
|
||||||
|
|
||||||
|
handler: (request, reply) ->
|
||||||
|
session.check request.body.session ? ""
|
||||||
|
.then (userid) ->
|
||||||
|
# active session found, get associated user
|
||||||
|
fftcgdb.get (userid)
|
||||||
|
.then (user) ->
|
||||||
|
logger.debug "OK '#{user.login}' got info"
|
||||||
|
reply.send
|
||||||
|
success: true
|
||||||
|
user: user
|
||||||
|
|
||||||
|
.catch (err) ->
|
||||||
|
# couldnt get user details
|
||||||
|
logger.warn "FAIL '#{err}' for user id '#{userid}'"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
|
|
||||||
|
.catch ->
|
||||||
|
# no session found
|
||||||
|
logger.info "FAIL '#{request.body.session}' session not found"
|
||||||
|
reply.send
|
||||||
|
success: false
|
||||||
24
backend/routes/user/info.schema.coffee
Normal file
24
backend/routes/user/info.schema.coffee
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
module.exports =
|
||||||
|
body:
|
||||||
|
session: type: 'string'
|
||||||
|
response:
|
||||||
|
200:
|
||||||
|
type: 'object'
|
||||||
|
required: ['success']
|
||||||
|
properties:
|
||||||
|
success: type: 'boolean'
|
||||||
|
user:
|
||||||
|
type: 'object'
|
||||||
|
required: ['user', 'login', 'settings']
|
||||||
|
properties:
|
||||||
|
user: type: 'integer'
|
||||||
|
login: type: 'string'
|
||||||
|
settings: type: 'string'
|
||||||
|
# user is required iff success
|
||||||
|
if:
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
const: true
|
||||||
|
then:
|
||||||
|
required: ['user']
|
||||||
|
else: true
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
logger = (require 'logging').default 'login'
|
logger = (require 'logging').default '/user/login'
|
||||||
|
|
||||||
# session storage (volatile data)
|
# session storage (volatile data)
|
||||||
session = (require '../../session')
|
session = (require '../../session')
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
logger = (require 'logging').default 'logout'
|
logger = (require 'logging').default '/user/logout'
|
||||||
|
|
||||||
# session storage (volatile data)
|
# session storage (volatile data)
|
||||||
session = (require '../../session')
|
session = (require '../../session')
|
||||||
|
|
@ -6,17 +6,7 @@ session = (require '../../session')
|
||||||
module.exports =
|
module.exports =
|
||||||
url: '/user/logout'
|
url: '/user/logout'
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
schema:
|
schema: (require './logout.schema')
|
||||||
body:
|
|
||||||
session: type: 'string'
|
|
||||||
response:
|
|
||||||
200:
|
|
||||||
type: 'object'
|
|
||||||
required: ['success']
|
|
||||||
properties:
|
|
||||||
success:
|
|
||||||
type: 'boolean'
|
|
||||||
const: true
|
|
||||||
|
|
||||||
handler: (request, reply) ->
|
handler: (request, reply) ->
|
||||||
new Promise (resolve) ->
|
new Promise (resolve) ->
|
||||||
|
|
|
||||||
11
backend/routes/user/logout.schema.coffee
Normal file
11
backend/routes/user/logout.schema.coffee
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports =
|
||||||
|
body:
|
||||||
|
session: type: 'string'
|
||||||
|
response:
|
||||||
|
200:
|
||||||
|
type: 'object'
|
||||||
|
required: ['success']
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: 'boolean'
|
||||||
|
const: true
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
logger = (require 'logging').default 'register'
|
logger = (require 'logging').default '/user/register'
|
||||||
|
|
||||||
# fftcg.db (persistent data)
|
# fftcg.db (persistent data)
|
||||||
fftcgdb = (require '../../db')
|
fftcgdb = (require '../../db')
|
||||||
|
|
@ -10,7 +10,7 @@ module.exports =
|
||||||
|
|
||||||
handler: (request, reply) ->
|
handler: (request, reply) ->
|
||||||
fftcgdb.register(request.body.login, request.body.password)
|
fftcgdb.register(request.body.login, request.body.password)
|
||||||
.then (user) ->
|
.then ->
|
||||||
logger.info "OK '#{request.body.login}'"
|
logger.info "OK '#{request.body.login}'"
|
||||||
reply.send
|
reply.send
|
||||||
success: true
|
success: true
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ fastify.route (require "./routes/#{route}") for route in [
|
||||||
'test'
|
'test'
|
||||||
# log in user
|
# log in user
|
||||||
'user/login'
|
'user/login'
|
||||||
|
# user info
|
||||||
|
'user/info'
|
||||||
# log out user
|
# log out user
|
||||||
'user/logout'
|
'user/logout'
|
||||||
# register user
|
# register user
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,6 @@ class FFTCGSESSION
|
||||||
else
|
else
|
||||||
@db.get digest, (err, res) ->
|
@db.get digest, (err, res) ->
|
||||||
logger.info "OK '#{digest}' resumed"
|
logger.info "OK '#{digest}' resumed"
|
||||||
resolve digest
|
resolve (JSON.parse res)
|
||||||
|
|
||||||
module.exports = new FFTCGSESSION
|
module.exports = new FFTCGSESSION
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export default {
|
||||||
let cookie_data = JSON.parse(response.data.message)
|
let cookie_data = JSON.parse(response.data.message)
|
||||||
Cookies.set('session', cookie_data.value, cookie_data.properties)
|
Cookies.set('session', cookie_data.value, cookie_data.properties)
|
||||||
this.$refs.main.showSnackbar('Login successful!', 'success')
|
this.$refs.main.showSnackbar('Login successful!', 'success')
|
||||||
this.$router.push('about')
|
this.$router.push('usercp')
|
||||||
} else {
|
} else {
|
||||||
this.$refs.main.showSnackbar(response.data.message, 'error')
|
this.$refs.main.showSnackbar(response.data.message, 'error')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@ export default new Router({
|
||||||
component: Home
|
component: Home
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: '/usercp',
|
||||||
name: 'about',
|
name: 'usercp',
|
||||||
// route level code-splitting
|
// route level code-splitting
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
// this generates a separate chunk (usercp.[hash].js) for this route
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "about" */ './views/About.vue')
|
import(/* webpackChunkName: "usercp" */ './views/UserCP.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/game',
|
path: '/game',
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,6 @@ import RegisterForm from '@/components/forms/Register.vue'
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
dialog: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
LoginForm,
|
LoginForm,
|
||||||
|
|
@ -38,7 +32,7 @@ export default {
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
this.$router.push({ name: 'about' })
|
this.$router.push({ name: 'usercp' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<v-container>
|
<v-container>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<p>user session: {{ sessionID }}</p>
|
<p>user logged in: {{ user.login }}</p>
|
||||||
<v-btn @click.native="logout">Logout</v-btn>
|
<v-btn @click.native="logout">Logout</v-btn>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -14,14 +14,14 @@ import axios from '@/plugins/axios'
|
||||||
import Header from '@/components/Header.vue'
|
import Header from '@/components/Header.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'About',
|
name: 'UserCP',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Header
|
Header
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
sessionID: ''
|
user: ''
|
||||||
}),
|
}),
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -45,12 +45,12 @@ export default {
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
axios
|
axios
|
||||||
.post('/user/login', {
|
.post('/user/info', {
|
||||||
session: Cookies.get('session')
|
session: Cookies.get('session')
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
this.sessionID = response.data.message
|
this.user = response.data.user
|
||||||
} else {
|
} else {
|
||||||
this.goHome()
|
this.goHome()
|
||||||
}
|
}
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
# libs
|
|
||||||
window.$ = require('jquery')
|
|
||||||
|
|
||||||
# on load
|
|
||||||
$ ->
|
|
||||||
# libs requiring full DOM
|
|
||||||
require 'craftyjs/dist/crafty'
|
|
||||||
io = require 'socket.io-client'
|
|
||||||
|
|
||||||
# style sheet
|
|
||||||
require './style/custom.scss'
|
|
||||||
|
|
||||||
# fftcg libs
|
|
||||||
require './game/config.coffee'
|
|
||||||
require './game/components/Card.coffee'
|
|
||||||
require './game/scenes/Battle.coffee'
|
|
||||||
|
|
||||||
# init Socket.IO
|
|
||||||
socket = io()
|
|
||||||
|
|
||||||
# init CraftyJS framework
|
|
||||||
Crafty.init()
|
|
||||||
|
|
||||||
# Load base scene
|
|
||||||
Crafty.scene "Battle"
|
|
||||||
|
|
||||||
# Testing some entities
|
|
||||||
Crafty.sprite 480, 670, '//www.fftcgmognet.com/images/cards/hd/1/1/107.jpg',
|
|
||||||
shantotto: [
|
|
||||||
0
|
|
||||||
0
|
|
||||||
]
|
|
||||||
|
|
||||||
backups = [
|
|
||||||
|
|
||||||
Crafty.e 'shantotto, AllyCard'
|
|
||||||
.attr {
|
|
||||||
card:
|
|
||||||
type: 'backup'
|
|
||||||
}
|
|
||||||
|
|
||||||
Crafty.e 'shantotto, AllyCard'
|
|
||||||
.attr {
|
|
||||||
card:
|
|
||||||
type: 'backup'
|
|
||||||
}
|
|
||||||
|
|
||||||
Crafty.e 'shantotto, AllyCard'
|
|
||||||
.attr {
|
|
||||||
card:
|
|
||||||
type: 'backup'
|
|
||||||
}
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
Crafty 'AllyCard'
|
|
||||||
.each (index) ->
|
|
||||||
switch @card.type
|
|
||||||
when 'backup'
|
|
||||||
@trigger 'Place',
|
|
||||||
x: CONF.coord.x.main + index * CONF.coord.x.step
|
|
||||||
y: CONF.coord.y.bkup
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
Crafty.e 'shantotto, EnemyCard'
|
|
||||||
.trigger 'Place',
|
|
||||||
x: 900
|
|
||||||
y: 0
|
|
||||||
|
|
||||||
return
|
|
||||||
105
src/index.coffee
105
src/index.coffee
|
|
@ -1,105 +0,0 @@
|
||||||
# libs
|
|
||||||
window.$ = require('jquery')
|
|
||||||
|
|
||||||
# import bootstrap
|
|
||||||
require './style/custom.scss'
|
|
||||||
require 'bootstrap/js/dist/alert'
|
|
||||||
|
|
||||||
window.showAlert = (level, content) ->
|
|
||||||
($ '.alert').alert 'close'
|
|
||||||
|
|
||||||
($ '#alert-area').append ($ '<div>',
|
|
||||||
class: "alert alert-#{level} alert-dismissible fade show"
|
|
||||||
role: 'alert'
|
|
||||||
.append content, ($ '<button>',
|
|
||||||
type: 'button'
|
|
||||||
class: 'close'
|
|
||||||
'data-dismiss': 'alert',
|
|
||||||
'aria-label': 'Close'
|
|
||||||
.append ($ '<span>',
|
|
||||||
'aria-hidden': 'true'
|
|
||||||
.append '×'
|
|
||||||
)))
|
|
||||||
|
|
||||||
# on load
|
|
||||||
$ ->
|
|
||||||
# reset forms
|
|
||||||
$('form').each ->
|
|
||||||
@fullReset = ->
|
|
||||||
$('input', @).each ->
|
|
||||||
$(@).removeClass 'is-invalid'
|
|
||||||
$(@).removeClass 'is-valid'
|
|
||||||
@reset()
|
|
||||||
|
|
||||||
# login form
|
|
||||||
$('form[name="login"]').submit (event) ->
|
|
||||||
that = @
|
|
||||||
# inhibit normal form submission
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
# gather form data
|
|
||||||
login = $('input[name="login"]', @)
|
|
||||||
password = $('input[name="password"]', @)
|
|
||||||
|
|
||||||
# transmit form data
|
|
||||||
$.post '/login',
|
|
||||||
login: login.val()
|
|
||||||
password: password.val()
|
|
||||||
.done (data) ->
|
|
||||||
if data.status == 'ok'
|
|
||||||
that.fullReset()
|
|
||||||
showAlert 'success', "successfully logged in '#{data.login}'"
|
|
||||||
location.reload()
|
|
||||||
|
|
||||||
else
|
|
||||||
switch data.text
|
|
||||||
when 'login'
|
|
||||||
showAlert 'warning', 'Invalid username and/or password.'
|
|
||||||
login.addClass 'is-invalid'
|
|
||||||
password.addClass 'is-invalid'
|
|
||||||
when 'db' or 'hash'
|
|
||||||
showAlert 'danger', 'Internal failure, try again later.'
|
|
||||||
else
|
|
||||||
showAlert 'danger', 'Unknown failure. Can you reproduce this?'
|
|
||||||
|
|
||||||
# registration form
|
|
||||||
$('form[name="register"]').submit (event) ->
|
|
||||||
that = @
|
|
||||||
# inhibit normal form submission
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
# gather form data
|
|
||||||
login = $('input[name="login"]', @)
|
|
||||||
password = $('input[name="password"]', @)
|
|
||||||
confirm = $('input[name="confirm"]', @)
|
|
||||||
|
|
||||||
# check form data
|
|
||||||
if password.val() != confirm.val()
|
|
||||||
confirm.addClass 'is-invalid'
|
|
||||||
confirm.focus()
|
|
||||||
|
|
||||||
else
|
|
||||||
# transmit form data
|
|
||||||
$.post '/register',
|
|
||||||
login: login.val()
|
|
||||||
password: password.val()
|
|
||||||
.done (data) ->
|
|
||||||
if data.status == 'ok'
|
|
||||||
that.fullReset()
|
|
||||||
showAlert 'success', "successfully registered '#{data.login}'"
|
|
||||||
|
|
||||||
else
|
|
||||||
switch data.text
|
|
||||||
when 'invalid'
|
|
||||||
showAlert 'warning', 'Invalid user input. Please provide username AND password.'
|
|
||||||
login.addClass 'is-invalid'
|
|
||||||
password.addClass 'is-invalid'
|
|
||||||
login.focus()
|
|
||||||
when 'hash'
|
|
||||||
showAlert 'danger', 'Internal failure, try again later.'
|
|
||||||
when 'db'
|
|
||||||
showAlert 'danger', 'Internal failure or user name already taken.'
|
|
||||||
login.addClass 'is-invalid'
|
|
||||||
login.focus()
|
|
||||||
else
|
|
||||||
showAlert 'danger', 'Unknown failure. Can you reproduce this?'
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# libs
|
|
||||||
window.$ = require('jquery')
|
|
||||||
|
|
||||||
# import bootstrap
|
|
||||||
require './style/custom.scss'
|
|
||||||
require 'bootstrap/js/dist/alert'
|
|
||||||
require 'bootstrap/js/dist/collapse'
|
|
||||||
|
|
||||||
# on load
|
|
||||||
$ ->
|
|
||||||
return
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
doctype html
|
|
||||||
html
|
|
||||||
head
|
|
||||||
title Crafty Things
|
|
||||||
script(src='/game.bundle.js')
|
|
||||||
body
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
doctype html
|
|
||||||
html
|
|
||||||
|
|
||||||
head
|
|
||||||
title Crafty Things
|
|
||||||
script(src='/index.bundle.js')
|
|
||||||
|
|
||||||
body
|
|
||||||
|
|
||||||
header.jumbotron.jumbotron-fluid.py-4.bg-primary.text-light.text-center
|
|
||||||
div.container
|
|
||||||
h1 Hello World!
|
|
||||||
h2 App under development, please don't submit any valuable data!
|
|
||||||
|
|
||||||
div.container.bg-light
|
|
||||||
div#alert-area
|
|
||||||
div.row
|
|
||||||
|
|
||||||
div.col-md-6
|
|
||||||
h3 Yavook!FFTCG
|
|
||||||
p Lorem ipsum dolor sit amet
|
|
||||||
|
|
||||||
div.col-md-3
|
|
||||||
h3 Login
|
|
||||||
form(name="login")
|
|
||||||
div.form-group
|
|
||||||
label(for="login") User name:
|
|
||||||
input.form-control(name="login" required)
|
|
||||||
|
|
||||||
div.form-group
|
|
||||||
label(for="password") Password:
|
|
||||||
input.form-control(name="password" type="password" required)
|
|
||||||
|
|
||||||
div.form-group
|
|
||||||
button.btn.btn-primary.w-100(type="submit") Login
|
|
||||||
|
|
||||||
div.col-md-3
|
|
||||||
h3 Register
|
|
||||||
form(name="register")
|
|
||||||
div.form-group
|
|
||||||
label(for="login") User name:
|
|
||||||
input.form-control(name="login" required)
|
|
||||||
div.invalid-feedback User name invalid or taken.
|
|
||||||
|
|
||||||
div.form-group
|
|
||||||
label(for="password") Password:
|
|
||||||
input.form-control(name="password" type="password" required)
|
|
||||||
|
|
||||||
label(for="confirm") Confirm password:
|
|
||||||
input.form-control(name="confirm" type="password")
|
|
||||||
div.invalid-feedback Passwords do not match.
|
|
||||||
|
|
||||||
button.btn.btn-primary.w-100(type="submit") Register
|
|
||||||
Reference in a new issue