implement recaptcha on login

pull/250/head
Henrique Dias 2017-09-11 09:00:59 +01:00
parent a61329843c
commit 002f8066c7
No known key found for this signature in database
GPG Key ID: 936F5EB68D786730
12 changed files with 473 additions and 330 deletions

View File

@ -8,6 +8,7 @@
<meta name="staticgen" content="{{ .StaticGen }}">
<meta name="noauth" content="{{ .NoAuth }}">
<meta name="version" content="{{ .Version }}">
<meta name="recaptcha" content="{{ .ReCaptchaKey }}">
<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="16x16" href="{{ .BaseURL }}/static/img/icons/favicon-16x16.png">
@ -27,6 +28,10 @@
<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 file of chunk.files) {
if (file.match(/\.(js|css)$/)) { %>

View File

@ -1,11 +1,38 @@
<template>
<router-view @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
<router-view :dependencies="loaded" @update:css="updateCSS" @clean:css="cleanCSS"></router-view>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'app',
computed: mapState(['recaptcha']),
data () {
return {
loaded: false
}
},
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.
let loading = document.getElementById('loading')
loading.classList.add('done')
@ -16,7 +43,6 @@ export default {
this.updateCSS()
},
methods: {
updateCSS (global = false) {
let css = this.$store.state.css

View File

@ -29,6 +29,14 @@
width: 90%;
}
#login.recaptcha form {
min-width: 304px;
}
#login #recaptcha {
margin: .5em 0 0;
}
#login input {
width: 100%;
width: 100%;

View File

@ -51,14 +51,10 @@ const router = new Router({
path: '/settings',
name: 'Settings',
component: Settings,
children: [
{
path: '/settings',
name: 'Settings',
redirect: {
path: '/settings/profile'
}
},
children: [
{
path: '/settings/profile',
name: 'Profile Settings',

View File

@ -17,6 +17,7 @@ const state = {
window.CSS = null
return css
})(),
recaptcha: document.querySelector('meta[name="recaptcha"]').getAttribute('content'),
staticGen: document.querySelector('meta[name="staticgen"]').getAttribute('content'),
baseURL: document.querySelector('meta[name="base"]').getAttribute('content'),
noAuth: (document.querySelector('meta[name="noauth"]').getAttribute('content') === 'true'),

View File

@ -31,8 +31,8 @@ function loggedIn () {
})
}
function login (user, password) {
let data = {username: user, password: password}
function login (user, password, captcha) {
let data = {username: user, password: password, recaptcha: captcha}
return new Promise((resolve, reject) => {
let request = new window.XMLHttpRequest()
request.open('POST', `${store.state.baseURL}/api/auth/get`, true)

View File

@ -1,11 +1,12 @@
<template>
<div id="login">
<div id="login" :class="{ recaptcha: recaptcha.length > 0 }">
<form @submit="submit">
<img src="../assets/logo.svg" alt="File Manager">
<h1>File Manager</h1>
<div v-if="wrong" class="wrong">{{ $t("login.wrongCredentials") }}</div>
<input type="text" v-model="username" :placeholder="$t('login.username')">
<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')">
</form>
</div>
@ -13,9 +14,12 @@
<script>
import auth from '@/utils/auth'
import { mapState } from 'vuex'
export default {
name: 'login',
props: ['dependencies'],
computed: mapState(['recaptcha']),
data: function () {
return {
wrong: false,
@ -23,8 +27,23 @@ export default {
password: ''
}
},
mounted () {
if (this.dependencies) this.setup()
},
watch: {
dependencies: function (val) {
if (val) this.setup()
}
},
methods: {
submit: function (event) {
setup () {
if (this.recaptcha.length === 0) return
window.grecaptcha.render('recaptcha', {
sitekey: this.recaptcha
})
},
submit (event) {
event.preventDefault()
event.stopPropagation()
@ -33,7 +52,17 @@ export default {
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 }) })
.catch(() => { this.wrong = true })
}

View File

@ -48,6 +48,8 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
scope := "."
database := ""
noAuth := false
reCaptchaKey := ""
reCaptchaSecret := ""
if plugin != "" {
baseURL = "/admin"
@ -155,6 +157,18 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
if u.ViewMode != "mosaic" && u.ViewMode != "list" {
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":
if !c.NextArg() {
noAuth = true
@ -216,6 +230,8 @@ func Parse(c *caddy.Controller, plugin string) ([]*filemanager.FileManager, erro
NoAuth: noAuth,
BaseURL: "",
PrefixURL: "",
ReCaptchaKey: reCaptchaKey,
ReCaptchaSecret: reCaptchaSecret,
DefaultUser: u,
Store: &filemanager.Store{
Config: bolt.ConfigStore{DB: db},

View File

@ -66,6 +66,10 @@ type FileManager struct {
// there will only exist one user, called "admin".
NoAuth bool
// ReCaptcha Site key and secret.
ReCaptchaKey string
ReCaptchaSecret string
// StaticGen is the static websit generator handler.
StaticGen StaticGen

View File

@ -1,8 +1,11 @@
package http
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
@ -11,6 +14,45 @@ import (
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.
func authHandler(c *fm.Context, w http.ResponseWriter, r *http.Request) (int, error) {
// 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.
var cred fm.User
var cred cred
if r.Body == 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
}
// 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.
u, err := c.Store.Users.GetByUsername(cred.Username, c.NewFS)
if err != nil {

View File

@ -230,6 +230,9 @@ func renderFile(c *fm.Context, w http.ResponseWriter, file string) (int, error)
"NoAuth": c.NoAuth,
"Version": fm.Version,
"CSS": template.CSS(c.CSS),
"ReCaptcha": c.ReCaptchaKey != "" && c.ReCaptchaSecret != "",
"ReCaptchaKey": c.ReCaptchaKey,
"ReCaptchaSecret": c.ReCaptchaSecret,
}
if c.StaticGen != nil {

File diff suppressed because one or more lines are too long