implement recaptcha on login
parent
a61329843c
commit
002f8066c7
|
@ -8,6 +8,7 @@
|
||||||
<meta name="staticgen" content="{{ .StaticGen }}">
|
<meta name="staticgen" content="{{ .StaticGen }}">
|
||||||
<meta name="noauth" content="{{ .NoAuth }}">
|
<meta name="noauth" content="{{ .NoAuth }}">
|
||||||
<meta name="version" content="{{ .Version }}">
|
<meta name="version" content="{{ .Version }}">
|
||||||
|
<meta name="recaptcha" content="{{ .ReCaptchaKey }}">
|
||||||
<title>File Manager</title>
|
<title>File Manager</title>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ .BaseURL }}/static/img/icons/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
|
||||||
|
@ -27,6 +28,10 @@
|
||||||
|
|
||||||
<script>CSS = "{{ .CSS }}"</script>
|
<script>CSS = "{{ .CSS }}"</script>
|
||||||
|
|
||||||
|
{{ if .ReCaptcha -}}
|
||||||
|
<script src='https://www.google.com/recaptcha/api.js?render=explicit'></script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<% for (var chunk of webpack.chunks) {
|
<% for (var chunk of webpack.chunks) {
|
||||||
for (var file of chunk.files) {
|
for (var file of chunk.files) {
|
||||||
if (file.match(/\.(js|css)$/)) { %>
|
if (file.match(/\.(js|css)$/)) { %>
|
||||||
|
|
|
@ -1,11 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<router-view @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
|
<router-view :dependencies="loaded" @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
computed: mapState(['recaptcha']),
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loaded: false
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
if (this.recaptcha.length === 0) {
|
||||||
|
this.unload()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let check = () => {
|
||||||
|
if (typeof window.grecaptcha === 'undefined') {
|
||||||
|
setTimeout(check, 100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.unload()
|
||||||
|
}
|
||||||
|
|
||||||
|
check()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unload () {
|
||||||
|
this.loaded = true
|
||||||
// Remove loading animation.
|
// Remove loading animation.
|
||||||
let loading = document.getElementById('loading')
|
let loading = document.getElementById('loading')
|
||||||
loading.classList.add('done')
|
loading.classList.add('done')
|
||||||
|
@ -16,7 +43,6 @@ export default {
|
||||||
|
|
||||||
this.updateCSS()
|
this.updateCSS()
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
updateCSS (global = false) {
|
updateCSS (global = false) {
|
||||||
let css = this.$store.state.css
|
let css = this.$store.state.css
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,14 @@
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#login.recaptcha form {
|
||||||
|
min-width: 304px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login #recaptcha {
|
||||||
|
margin: .5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
#login input {
|
#login input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -51,14 +51,10 @@ const router = new Router({
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
component: Settings,
|
component: Settings,
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
name: 'Settings',
|
|
||||||
redirect: {
|
redirect: {
|
||||||
path: '/settings/profile'
|
path: '/settings/profile'
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
path: '/settings/profile',
|
path: '/settings/profile',
|
||||||
name: 'Profile Settings',
|
name: 'Profile Settings',
|
||||||
|
|
|
@ -17,6 +17,7 @@ const state = {
|
||||||
window.CSS = null
|
window.CSS = null
|
||||||
return css
|
return css
|
||||||
})(),
|
})(),
|
||||||
|
recaptcha: document.querySelector('meta[name="recaptcha"]').getAttribute('content'),
|
||||||
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
|
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
|
||||||
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
|
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
|
||||||
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),
|
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),
|
||||||
|
|
|
@ -31,8 +31,8 @@ function loggedIn () {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function login (user, password) {
|
function login (user, password, captcha) {
|
||||||
let data = {username: user, password: password}
|
let data = {username: user, password: password, recaptcha: captcha}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let request = new window.XMLHttpRequest()
|
let request = new window.XMLHttpRequest()
|
||||||
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)
|
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="login">
|
<div id="login" :class="{ recaptcha: recaptcha.length > 0 }">
|
||||||
<form @submit="submit">
|
<form @submit="submit">
|
||||||
<img src="../assets/logo.svg" alt="File Manager">
|
<img src="../assets/logo.svg" alt="File Manager">
|
||||||
<h1>File Manager</h1>
|
<h1>File Manager</h1>
|
||||||
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
|
||||||
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
<input type="text" v-model="username" :placeholder="$t('login.username')">
|
||||||
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
<input type="password" v-model="password" :placeholder="$t('login.password')">
|
||||||
|
<div v-if="recaptcha.length" id="recaptcha"></div>
|
||||||
<input type="submit" :value="$t('login.submit')">
|
<input type="submit" :value="$t('login.submit')">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,9 +14,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import auth from '@/utils/auth'
|
import auth from '@/utils/auth'
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
props: ['dependencies'],
|
||||||
|
computed: mapState(['recaptcha']),
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
wrong: false,
|
wrong: false,
|
||||||
|
@ -23,8 +27,23 @@ export default {
|
||||||
password: ''
|
password: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
if (this.dependencies) this.setup()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dependencies: function (val) {
|
||||||
|
if (val) this.setup()
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit: function (event) {
|
setup () {
|
||||||
|
if (this.recaptcha.length === 0) return
|
||||||
|
|
||||||
|
window.grecaptcha.render('recaptcha', {
|
||||||
|
sitekey: this.recaptcha
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submit (event) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
|
@ -33,7 +52,17 @@ export default {
|
||||||
redirect = '/files/'
|
redirect = '/files/'
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.login(this.username, this.password)
|
let captcha = ''
|
||||||
|
if (this.recaptcha.length > 0) {
|
||||||
|
captcha = window.grecaptcha.getResponse()
|
||||||
|
|
||||||
|
if (captcha === '') {
|
||||||
|
this.wrong = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.login(this.username, this.password, captcha)
|
||||||
.then(() => { this.$router.push({ path: redirect }) })
|
.then(() => { this.$router.push({ path: redirect }) })
|
||||||
.catch(() => { this.wrong = true })
|
.catch(() => { this.wrong = true })
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,8 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
|
||||||
scope := "."
|
scope := "."
|
||||||
database := ""
|
database := ""
|
||||||
noAuth := false
|
noAuth := false
|
||||||
|
reCaptchaKey := ""
|
||||||
|
reCaptchaSecret := ""
|
||||||
|
|
||||||
if plugin != "" {
|
if plugin != "" {
|
||||||
baseURL = "/admin"
|
baseURL = "/admin"
|
||||||
|
@ -155,6 +157,18 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
|
||||||
if u.ViewMode != "mosaic" && u.ViewMode != "list" {
|
if u.ViewMode != "mosaic" && u.ViewMode != "list" {
|
||||||
return nil, c.ArgErr()
|
return nil, c.ArgErr()
|
||||||
}
|
}
|
||||||
|
case "recaptcha_key":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
reCaptchaKey = c.Val()
|
||||||
|
case "recaptcha_secret":
|
||||||
|
if !c.NextArg() {
|
||||||
|
return nil, c.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
reCaptchaSecret = c.Val()
|
||||||
case "no_auth":
|
case "no_auth":
|
||||||
if !c.NextArg() {
|
if !c.NextArg() {
|
||||||
noAuth = true
|
noAuth = true
|
||||||
|
@ -216,6 +230,8 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
|
||||||
NoAuth: noAuth,
|
NoAuth: noAuth,
|
||||||
BaseURL: "",
|
BaseURL: "",
|
||||||
PrefixURL: "",
|
PrefixURL: "",
|
||||||
|
ReCaptchaKey: reCaptchaKey,
|
||||||
|
ReCaptchaSecret: reCaptchaSecret,
|
||||||
DefaultUser: u,
|
DefaultUser: u,
|
||||||
Store: &filemanager.Store{
|
Store: &filemanager.Store{
|
||||||
Config: bolt.ConfigStore{DB: db},
|
Config: bolt.ConfigStore{DB: db},
|
||||||
|
|
|
@ -66,6 +66,10 @@ type FileManager struct {
|
||||||
// there will only exist one user, called "admin".
|
// there will only exist one user, called "admin".
|
||||||
NoAuth bool
|
NoAuth bool
|
||||||
|
|
||||||
|
// ReCaptcha Site key and secret.
|
||||||
|
ReCaptchaKey string
|
||||||
|
ReCaptchaSecret string
|
||||||
|
|
||||||
// StaticGen is the static websit generator handler.
|
// StaticGen is the static websit generator handler.
|
||||||
StaticGen StaticGen
|
StaticGen StaticGen
|
||||||
|
|
||||||
|
|
57
http/auth.go
57
http/auth.go
|
@ -1,8 +1,11 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -11,6 +14,45 @@ import (
|
||||||
fm "github.com/hacdias/filemanager"
|
fm "github.com/hacdias/filemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type cred struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Recaptcha string `json:"recaptcha"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// recaptcha checks the recaptcha code.
|
||||||
|
func recaptcha(secret string, response string) (bool, error) {
|
||||||
|
api := "https://www.google.com/recaptcha/api/siteverify"
|
||||||
|
|
||||||
|
body := url.Values{}
|
||||||
|
body.Set("secret", secret)
|
||||||
|
body.Add("response", response)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Post(api, "application/x-www-form-urlencoded", bytes.NewBufferString(body.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ChallengeTS time.Time `json:"challenge_ts"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
ErrorCodes interface{} `json:"error-codes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&data)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Success, nil
|
||||||
|
}
|
||||||
|
|
||||||
// authHandler proccesses the authentication for the user.
|
// authHandler proccesses the authentication for the user.
|
||||||
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
// NoAuth instances shouldn't call this method.
|
// NoAuth instances shouldn't call this method.
|
||||||
|
@ -19,7 +61,7 @@ func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, er
|
||||||
}
|
}
|
||||||
|
|
||||||
// Receive the credentials from the request and unmarshal them.
|
// Receive the credentials from the request and unmarshal them.
|
||||||
var cred fm.User
|
var cred cred
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
@ -29,6 +71,19 @@ func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, er
|
||||||
return http.StatusForbidden, nil
|
return http.StatusForbidden, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If ReCaptcha is enabled, check the code.
|
||||||
|
if len(c.ReCaptchaSecret) > 0 {
|
||||||
|
ok, err := recaptcha(c.ReCaptchaSecret, cred.Recaptcha)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return http.StatusForbidden, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return http.StatusForbidden, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if the user exists.
|
// Checks if the user exists.
|
||||||
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
|
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -230,6 +230,9 @@ func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error)
|
||||||
"NoAuth": c.NoAuth,
|
"NoAuth": c.NoAuth,
|
||||||
"Version": fm.Version,
|
"Version": fm.Version,
|
||||||
"CSS": template.CSS(c.CSS),
|
"CSS": template.CSS(c.CSS),
|
||||||
|
"ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
|
||||||
|
"ReCaptchaKey": c.ReCaptchaKey,
|
||||||
|
"ReCaptchaSecret": c.ReCaptchaSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.StaticGen != nil {
|
if c.StaticGen != nil {
|
||||||
|
|
602
rice-box.go
602
rice-box.go
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue