users #16
|
@ -0,0 +1,123 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"code.chrissexton.org/cws/cabinet/config"
|
||||
"code.chrissexton.org/cws/cabinet/db"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
*db.Database
|
||||
|
||||
ID int64
|
||||
Name string
|
||||
Hash []byte
|
||||
AuthKey string `db:"auth_key"`
|
||||
Invalidate time.Time
|
||||
}
|
||||
|
||||
func PrepareTable(tx *sqlx.Tx) error {
|
||||
q := `create table if not exists users (
|
||||
id integer primary key,
|
||||
name text unique not null,
|
||||
hash text not null,
|
||||
auth_key text,
|
||||
invalidate datetime
|
||||
)`
|
||||
_, err := tx.Exec(q)
|
||||
return err
|
||||
}
|
||||
|
||||
func makeKey() (string, error) {
|
||||
keySize := config.GetInt("key.size", 10)
|
||||
buf := make([]byte, keySize)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key := hex.EncodeToString(buf)
|
||||
log.Debug().Msgf("Encoded secret key %s as %s", string(buf), key)
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func New(db *db.Database, name, password string) (*User, error) {
|
||||
q := `insert into users values (null, ?, ?, ?, ?)`
|
||||
|
||||
key, err := makeKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invalidate := time.Now().Add(time.Duration(config.GetInt("invalidate.hours", 7*24)) * time.Hour)
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), config.GetInt("hash.cost", 10))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := db.Exec(q, name, hash, key, invalidate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := &User{
|
||||
ID: id,
|
||||
Name: name,
|
||||
AuthKey: key,
|
||||
Invalidate: invalidate,
|
||||
}
|
||||
u.Set(password)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func Get(db *db.Database, name string) (*User, error) {
|
||||
q := `select * from users where name = ?`
|
||||
u := &User{}
|
||||
if err := db.Get(u, q, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (u *User) Set(newPassword string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Hash = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) Validate(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword(u.Hash, []byte(password))
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("incorrect credentials")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func GetByKey(db *db.Database, key string) (*User, error) {
|
||||
q := `select * from users where auth_key = ?`
|
||||
u := &User{}
|
||||
invalid := errors.New("invalid key")
|
||||
if err := db.Get(u, q, key); err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
return nil, invalid
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if u.Invalidate.Before(time.Now()) {
|
||||
return nil, invalid
|
||||
}
|
||||
return u, nil
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"code.chrissexton.org/cws/cabinet/db"
|
||||
)
|
||||
|
||||
func TestMakeKey(t *testing.T) {
|
||||
k, err := makeKey()
|
||||
assert.Nil(t, err)
|
||||
assert.NotEmpty(t, k)
|
||||
}
|
||||
|
||||
func TestGetByKey(t *testing.T) {
|
||||
d, err := db.New(":memory:")
|
||||
assert.Nil(t, err)
|
||||
tx, err := d.Beginx()
|
||||
assert.Nil(t, err)
|
||||
err = PrepareTable(tx)
|
||||
assert.Nil(t, err)
|
||||
err = tx.Commit()
|
||||
assert.Nil(t, err)
|
||||
u, err := New(d, "test", "abc")
|
||||
assert.Nil(t, err)
|
||||
u2, err := GetByKey(d, u.AuthKey)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, u.ID, u2.ID)
|
||||
}
|
||||
|
||||
func TestGetByKeyFailure(t *testing.T) {
|
||||
d, err := db.New(":memory:")
|
||||
assert.Nil(t, err)
|
||||
tx, err := d.Beginx()
|
||||
assert.Nil(t, err)
|
||||
err = PrepareTable(tx)
|
||||
assert.Nil(t, err)
|
||||
err = tx.Commit()
|
||||
assert.Nil(t, err)
|
||||
_, err = New(d, "test", "abc")
|
||||
assert.Nil(t, err)
|
||||
u2, err := GetByKey(d, "foobar")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, u2)
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
d, err := db.New(":memory:")
|
||||
assert.Nil(t, err)
|
||||
tx, err := d.Beginx()
|
||||
assert.Nil(t, err)
|
||||
err = PrepareTable(tx)
|
||||
assert.Nil(t, err)
|
||||
err = tx.Commit()
|
||||
assert.Nil(t, err)
|
||||
u, err := New(d, "test", "abc")
|
||||
assert.Nil(t, err)
|
||||
u2, err := Get(d, "test")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, u.ID, u2.ID)
|
||||
}
|
||||
|
||||
func TestUser_Validate(t *testing.T) {
|
||||
d, err := db.New(":memory:")
|
||||
assert.Nil(t, err)
|
||||
tx, err := d.Beginx()
|
||||
assert.Nil(t, err)
|
||||
err = PrepareTable(tx)
|
||||
assert.Nil(t, err)
|
||||
err = tx.Commit()
|
||||
assert.Nil(t, err)
|
||||
u, err := New(d, "test", "abc")
|
||||
assert.Nil(t, err)
|
||||
actual := u.Validate("abc")
|
||||
assert.True(t, actual)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetInt(key string, fallback int) int {
|
||||
v := Get(key, strconv.Itoa(fallback))
|
||||
if out, err := strconv.Atoi(v); err == nil {
|
||||
return out
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func Get(key, fallback string) string {
|
||||
key = strings.ToUpper(key)
|
||||
key = strings.ReplaceAll(key, ".", "_")
|
||||
if v, found := os.LookupEnv(key); found {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
2
db/db.go
2
db/db.go
|
@ -2,6 +2,8 @@ package db
|
|||
|
||||
import "github.com/jmoiron/sqlx"
|
||||
|
||||
import _ "github.com/mattn/go-sqlite3"
|
||||
|
||||
type Database struct {
|
||||
*sqlx.DB
|
||||
}
|
||||
|
|
|
@ -8,17 +8,19 @@
|
|||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli": "^4.0.5",
|
||||
"asciidoctor": "^2.0.3",
|
||||
"axios": "^0.19.0",
|
||||
"jquery": "^1.9.1",
|
||||
"popper.js": "^1.14.7",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bootstrap-vue": "^2.0.4",
|
||||
"bootswatch": "^4.3.1",
|
||||
"brace": "latest",
|
||||
"core-js": "^3.3.2",
|
||||
"jquery": "^1.9.1",
|
||||
"lodash": "^4.17.15",
|
||||
"popper.js": "^1.14.7",
|
||||
"vue": "^2.6.10",
|
||||
"vue-cookies": "^1.7.0",
|
||||
"vue-router": "^3.1.3",
|
||||
"vue2-ace-editor": "^0.0.15",
|
||||
"vuex": "^3.0.1"
|
||||
|
@ -34,7 +36,7 @@
|
|||
"eslint-plugin-vue": "^5.0.0",
|
||||
"sass": "^1.23.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"webpack": "^4.36.0",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"webpack": "^4.36.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<b-form @submit="onSubmit" @reset="onReset">
|
||||
<b-form-group
|
||||
id="username"
|
||||
label="Username"
|
||||
label-for="username-input">
|
||||
<b-form-input
|
||||
id="username-input"
|
||||
v-model="form.username"
|
||||
required></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
id="password"
|
||||
label="Password"
|
||||
label-for="password-input">
|
||||
<b-form-input
|
||||
id="password-input"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required></b-form-input>
|
||||
</b-form-group>
|
||||
<b-form-group class="justify-content-md-center align-content-center">
|
||||
<b-button
|
||||
id="login-button"
|
||||
type="submit"
|
||||
variant="primary">Submit
|
||||
</b-button>
|
||||
<b-button
|
||||
id="reset-button"
|
||||
type="reset"
|
||||
variant="danger">Reset
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
</b-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Login",
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit(evt) {
|
||||
evt.preventDefault()
|
||||
this.$store.dispatch('login', {username: this.form.username, password: this.form.password})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.then(() => {
|
||||
this.$cookies.set('key', this.$store.state.key)
|
||||
if (!this.$route.params.returnTo)
|
||||
this.$router.push("/")
|
||||
else
|
||||
this.$router.push(this.$route.params.returnTo)
|
||||
})
|
||||
},
|
||||
onReset(evt) {
|
||||
evt.preventDefault()
|
||||
this.form.username = ''
|
||||
this.form.password = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -4,6 +4,7 @@ import router from './router'
|
|||
import store from './store'
|
||||
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import VueCookies from 'vue-cookies'
|
||||
|
||||
// import "bootswatch/dist/darkly/variables";
|
||||
// import "bootstrap/scss/bootstrap";
|
||||
|
@ -15,6 +16,7 @@ import 'bootstrap-vue/dist/bootstrap-vue.css'
|
|||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
Vue.use(VueCookies)
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Login from '../views/Login.vue'
|
||||
import Console from '../views/Console.vue'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
@ -31,6 +32,11 @@ const routes = [
|
|||
name: 'console',
|
||||
component: Console
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
|
|
|
@ -23,7 +23,8 @@ export default new Vuex.Store({
|
|||
errs: [],
|
||||
searchResults: [],
|
||||
query: null,
|
||||
file: null
|
||||
file: null,
|
||||
key: null
|
||||
},
|
||||
mutations: {
|
||||
clearError(state) {
|
||||
|
@ -46,11 +47,18 @@ export default new Vuex.Store({
|
|||
},
|
||||
setTags(state, tags) {
|
||||
state.file.Tags = tags
|
||||
},
|
||||
setKey(state, key) {
|
||||
axios.defaults.headers.common['X-Auth-Key'] = key
|
||||
state.key = key
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
newFile: function({commit}) {
|
||||
newFile: function ({commit, state}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!state.key) {
|
||||
return reject('not logged in')
|
||||
}
|
||||
axios.post('/v1/entries', {})
|
||||
.catch(err => {
|
||||
commit('addError', err)
|
||||
|
@ -85,10 +93,14 @@ export default new Vuex.Store({
|
|||
})
|
||||
},
|
||||
saveFile: function ({state}) {
|
||||
if (!state.key)
|
||||
return new Promise((resolve, reject) => { reject('not logged in') })
|
||||
if (state.file)
|
||||
return axios.put('/v1/entries/' + state.file.Slug, state.file)
|
||||
},
|
||||
deleteBySlug: function({dispatch,commit}, slug) {
|
||||
deleteBySlug: function ({dispatch, commit, state}, slug) {
|
||||
if (!state.key)
|
||||
return new Promise((resolve, reject) => { reject('not logged in') })
|
||||
axios.delete('/v1/entries/' + slug)
|
||||
.catch(err => {
|
||||
commit('addError', err)
|
||||
|
@ -96,8 +108,21 @@ export default new Vuex.Store({
|
|||
.then(() => {
|
||||
dispatch('updateSearch')
|
||||
})
|
||||
},
|
||||
login: function ({commit}, {username, password}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.post('/v1/auth', {username: username, password: password})
|
||||
.then(res => {
|
||||
commit('setKey', res.data.User.AuthKey)
|
||||
commit('clearError')
|
||||
resolve()
|
||||
})
|
||||
.catch(err => {
|
||||
commit('addError', err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
modules: {}
|
||||
})
|
||||
|
|
|
@ -78,7 +78,16 @@ export default {
|
|||
// called before the route that renders this component is confirmed.
|
||||
// does NOT have access to `this` component instance,
|
||||
// because it has not been created yet when this guard is called!
|
||||
next()
|
||||
next(vm => {
|
||||
if (!vm.$store.state.key) {
|
||||
let key = vm.$cookies.get('key')
|
||||
if (key) {
|
||||
vm.$store.commit('setKey', key)
|
||||
return
|
||||
}
|
||||
vm.$router.push({name: "login", params: {returnTo: vm.$route.path}})
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
// called when the route that renders this component has changed,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<b-container fluid>
|
||||
<b-row><b-col>
|
||||
<h1>Login</h1>
|
||||
</b-col></b-row>
|
||||
<b-row class="justify-content-md-center">
|
||||
<b-col md="auto">
|
||||
<Login />
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import Login from '../components/Login.vue'
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
components: {
|
||||
Login
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
h2 {
|
||||
font-size: large;
|
||||
}
|
||||
</style>
|
6061
frontend/yarn.lock
6061
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
2
go.mod
2
go.mod
|
@ -11,6 +11,8 @@ require (
|
|||
github.com/rs/zerolog v1.16.0
|
||||
github.com/speps/go-hashids v2.0.0+incompatible
|
||||
github.com/stretchr/graceful v1.2.15
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6
|
||||
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
)
|
||||
|
|
32
go.sum
32
go.sum
|
@ -6,43 +6,64 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
|||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
|
||||
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
|
||||
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
|
||||
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
|
||||
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
||||
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
|
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
||||
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
|
||||
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
|
||||
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
|
||||
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY=
|
||||
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.16.0 h1:AaELmZdcJHT8m6oZ5py4213cdFK8XGXkB3dFdAQ+P7Q=
|
||||
github.com/rs/zerolog v1.16.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/speps/go-hashids v1.0.0 h1:jdFC07PrExRM4Og5Ev4411Tox75aFpkC77NlmutadNI=
|
||||
github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw=
|
||||
github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
|
@ -50,22 +71,29 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL
|
|||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/graceful v1.2.15 h1:vmXbwPGfe8bI6KkgmHry/P1Pk63bM3TDcfi+5mh+VHg=
|
||||
github.com/stretchr/graceful v1.2.15/go.mod h1:IxdGAOTZueMKoBr3oJIzdeg5CCCXbHXfV44sLhfAXXI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6 h1:VsmCukA2gDdC3Mu6evOIT0QjLSQWiJIwzv1Bdj4jdzU=
|
||||
golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -75,13 +103,17 @@ golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c h1:S/FtSvpNLtFBgjTqcKsRpsa6aVsI6iztaz1bQd9BJwE=
|
||||
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
8
main.go
8
main.go
|
@ -1,10 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"code.chrissexton.org/cws/cabinet/entry"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"code.chrissexton.org/cws/cabinet/auth"
|
||||
"code.chrissexton.org/cws/cabinet/entry"
|
||||
|
||||
"code.chrissexton.org/cws/cabinet/db"
|
||||
"code.chrissexton.org/cws/cabinet/web"
|
||||
|
||||
|
@ -22,6 +24,7 @@ var (
|
|||
salt = flag.String("salt", "c4b1n3t", "salt for IDs")
|
||||
minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size")
|
||||
develop = flag.Bool("develop", false, "turn on develop mode")
|
||||
adminSecret = flag.String("adminSecret", "helpme123", "secret for user creation")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -43,6 +46,9 @@ func main() {
|
|||
if err := entry.PrepareTable(tx); err != nil {
|
||||
log.Fatal().Err(err).Msg("could not create database")
|
||||
}
|
||||
if err = auth.PrepareTable(tx); err != nil {
|
||||
log.Fatal().Err(err).Msg("could not create database")
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
s := web.New(*httpAddr, db, box)
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.chrissexton.org/cws/cabinet/auth"
|
||||
"code.chrissexton.org/cws/cabinet/config"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (web *Web) auth(w http.ResponseWriter, r *http.Request) {
|
||||
req := struct {
|
||||
Username string
|
||||
Password string
|
||||
}{}
|
||||
dec := json.NewDecoder(r.Body)
|
||||
err := dec.Decode(&req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error decoding json request")
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth.Get(web.db, req.Username)
|
||||
if err != nil {
|
||||
w.WriteHeader(401)
|
||||
resp := struct {
|
||||
Status bool
|
||||
Err string
|
||||
}{
|
||||
false,
|
||||
"User and password combination is invalid",
|
||||
}
|
||||
j, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
log.Error().Err(err).Msg("Error encoding json response")
|
||||
return
|
||||
}
|
||||
w.Write(j)
|
||||
return
|
||||
}
|
||||
|
||||
if user.Validate(req.Password) {
|
||||
resp := struct {
|
||||
Status bool
|
||||
User auth.User
|
||||
}{
|
||||
true,
|
||||
*user,
|
||||
}
|
||||
j, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
log.Error().Err(err).Msg("Error encoding json response")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Write(j)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(401)
|
||||
resp := struct {
|
||||
Status bool
|
||||
Message string
|
||||
}{Message: "incorrect credentials"}
|
||||
j, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
log.Error().Err(err).Msg("Error encoding json response")
|
||||
return
|
||||
}
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (web *Web) newUser(w http.ResponseWriter, r *http.Request) {
|
||||
secret := r.Header.Get("X-secret")
|
||||
if secret != config.Get("secret", "abc123") {
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
dec := json.NewDecoder(r.Body)
|
||||
req := struct {
|
||||
Username string
|
||||
Password string
|
||||
}{}
|
||||
err := dec.Decode(&req)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, err)
|
||||
return
|
||||
}
|
||||
_, err = auth.New(web.db, req.Username, req.Password)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, err)
|
||||
log.Error().Err(err).Msg("Could not create user")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"code.chrissexton.org/cws/cabinet/auth"
|
||||
"code.chrissexton.org/cws/cabinet/db"
|
||||
|
||||
packr "github.com/gobuffalo/packr/v2"
|
||||
|
||||
"github.com/speps/go-hashids"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/graceful"
|
||||
)
|
||||
|
||||
|
@ -21,7 +22,6 @@ type Web struct {
|
|||
addr string
|
||||
db *db.Database
|
||||
salt string
|
||||
h *hashids.HashID
|
||||
box *packr.Box
|
||||
}
|
||||
|
||||
|
@ -39,9 +39,31 @@ func New(addr string, db *db.Database, box *packr.Box) *Web {
|
|||
return w
|
||||
}
|
||||
|
||||
type AuthMiddleware struct {
|
||||
db *db.Database
|
||||
}
|
||||
|
||||
func (aw *AuthMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.Header.Get("X-Auth-Key")
|
||||
u, err := auth.GetByKey(aw.db, key)
|
||||
if key == "" || err != nil {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprint(w, "invalid login")
|
||||
return
|
||||
}
|
||||
log.Debug().Msgf("This shit is authed to user %s!", u.Name)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (web *Web) routeSetup() http.Handler {
|
||||
r := mux.NewRouter()
|
||||
api := r.PathPrefix("/v1/").Subrouter()
|
||||
auth := AuthMiddleware{web.db}
|
||||
|
||||
authedApi := r.PathPrefix("/v1/").Subrouter()
|
||||
authedApi.Use(auth.Middleware)
|
||||
|
||||
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug().Msg("test json")
|
||||
|
@ -53,15 +75,19 @@ func (web *Web) routeSetup() http.Handler {
|
|||
|
||||
// curl 'http://127.0.0.1:8080/v1/test' -X POST -H 'Accept: application/json, text/plain, */*' --compressed -H 'Content-Type: application/json;charset=utf-8' --data '{ "test": 1 }'
|
||||
|
||||
api.HandleFunc("/entries", web.allEntries).Methods(http.MethodGet)
|
||||
api.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost)
|
||||
api.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost).
|
||||
authedApi.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost)
|
||||
authedApi.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost).
|
||||
HeadersRegexp("Content-Type", "application/(text|json).*")
|
||||
api.HandleFunc("/entries", web.newMarkdownEntry).Methods(http.MethodPost).
|
||||
authedApi.HandleFunc("/entries", web.newMarkdownEntry).Methods(http.MethodPost).
|
||||
HeadersRegexp("Content-Type", "application/markdown.*")
|
||||
api.HandleFunc("/entries/{slug}", web.removeEntry).Methods(http.MethodDelete)
|
||||
api.HandleFunc("/entries/{slug}", web.editEntry).Methods(http.MethodPut)
|
||||
authedApi.HandleFunc("/entries/{slug}", web.removeEntry).Methods(http.MethodDelete)
|
||||
authedApi.HandleFunc("/entries/{slug}", web.editEntry).Methods(http.MethodPut)
|
||||
|
||||
api.HandleFunc("/entries/{slug}", web.getEntry).Methods(http.MethodGet)
|
||||
api.HandleFunc("/entries", web.allEntries).Methods(http.MethodGet)
|
||||
|
||||
api.HandleFunc("/auth/new", web.newUser).Methods(http.MethodPost)
|
||||
api.HandleFunc("/auth", web.auth).Methods(http.MethodPost)
|
||||
r.PathPrefix("/").HandlerFunc(web.indexHandler("/index.html"))
|
||||
loggedRouter := handlers.LoggingHandler(os.Stdout, r)
|
||||
return loggedRouter
|
||||
|
|
Loading…
Reference in New Issue