Compare commits
27 Commits
Author | SHA1 | Date |
---|---|---|
Chris Sexton | 6988064309 | |
cws | b15a85131f | |
Chris Sexton | 7f077cfe31 | |
Chris Sexton | 68b485c36f | |
Chris Sexton | d9a688dbd1 | |
cws | e504a660ae | |
Chris Sexton | 8a785f964f | |
cws | bd7568a159 | |
Chris Sexton | a1d39c3930 | |
cws | 191dbfa23a | |
Chris Sexton | 516f4610dc | |
Chris Sexton | 1b8e5b4d70 | |
Chris Sexton | ce02dca041 | |
Chris Sexton | b3eda2c3cf | |
Chris Sexton | 98d9d8a899 | |
cws | 2fdf9ce7e6 | |
Chris Sexton | c43f8c18ca | |
Chris Sexton | def6e6f24f | |
Chris Sexton | e80b138f67 | |
Chris Sexton | 6b350ef201 | |
Chris Sexton | 3c39830c4d | |
Chris Sexton | 0d32edbe0e | |
Chris Sexton | 723f628c31 | |
Chris Sexton | ceaa8e6a96 | |
Chris Sexton | e9360e5082 | |
Chris Sexton | b67a1cccd5 | |
Chris Sexton | 08894f1ef4 |
|
@ -264,3 +264,4 @@ frontend/dist
|
||||||
*-packr.go
|
*-packr.go
|
||||||
env.make
|
env.make
|
||||||
cabinet
|
cabinet
|
||||||
|
esc.go
|
||||||
|
|
|
@ -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 or replace 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/jmoiron/sqlx"
|
||||||
|
|
||||||
|
import _ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
type Database struct {
|
type Database struct {
|
||||||
*sqlx.DB
|
*sqlx.DB
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
FROM golang:alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
RUN apk add --no-cache musl-dev
|
||||||
|
RUN apk add --no-cache gcc
|
||||||
|
RUN apk add --no-cache sqlite
|
||||||
|
RUN apk add --no-cache go
|
||||||
|
RUN apk add --no-cache make
|
||||||
|
RUN apk add --no-cache npm
|
||||||
|
RUN apk add --no-cache yarn
|
||||||
|
|
||||||
|
VOLUME /app/var
|
||||||
|
EXPOSE 5673
|
||||||
|
|
||||||
|
ARG gomaxprocs="8"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV SRC_DIR=/app/src/
|
||||||
|
|
||||||
|
ENV GOMAXPROCS=${gomaxprocs}
|
||||||
|
|
||||||
|
RUN git clone https://code.chrissexton.org/cws/cabinet.git $SRC_DIR
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
|
ENV TZ America/New_York
|
||||||
|
|
||||||
|
# RUN yarn global add @vue/cli
|
||||||
|
RUN cd $SRC_DIR/frontend; yarn && yarn build
|
||||||
|
RUN go get -u github.com/mjibson/esc
|
||||||
|
RUN cd $SRC_DIR; go generate && go get ./... && go build -o /app/cabinet
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/cabinet", "-httpAddr=0.0.0.0:5673", "-db=/app/var/cabinet.db"]
|
119
entry/entry.go
119
entry/entry.go
|
@ -2,19 +2,23 @@ package entry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.chrissexton.org/cws/cabinet/db"
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"code.chrissexton.org/cws/cabinet/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
db *db.Database
|
db *db.Database
|
||||||
ID int64
|
ID int64
|
||||||
Slug string
|
Slug string
|
||||||
|
Title string
|
||||||
Content string
|
Content string
|
||||||
Tags []string
|
Tags []string
|
||||||
Created time.Time
|
Created time.Time
|
||||||
|
@ -51,13 +55,53 @@ func PrepareTable(tx *sqlx.Tx) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(db *db.Database) Entry {
|
func NewFromAdoc(db *db.Database, body string) *Entry {
|
||||||
return Entry{
|
e := New(db)
|
||||||
|
e.Content = body
|
||||||
|
e.Title = e.GenerateTitle()
|
||||||
|
e.Slug = e.UniqueSlug()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func pandocMdToAdoc(body string) string {
|
||||||
|
log.Debug().Str("input", body).Msgf("converting md->adoc")
|
||||||
|
cmd := exec.Command("pandoc", "-f", "commonmark", "-t", "asciidoctor")
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not get stdin")
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer stdin.Close()
|
||||||
|
io.WriteString(stdin, body)
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not get stdout")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("md->adoc: %s", out)
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromMd(db *db.Database, body string) *Entry {
|
||||||
|
body = pandocMdToAdoc(body)
|
||||||
|
return NewFromAdoc(db, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *db.Database) *Entry {
|
||||||
|
e := Entry{
|
||||||
db: db,
|
db: db,
|
||||||
ID: -1,
|
ID: -1,
|
||||||
Created: time.Now(),
|
Created: time.Now(),
|
||||||
Updated: time.Now(),
|
Updated: time.Now(),
|
||||||
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
e.Title = e.GenerateTitle()
|
||||||
|
e.Slug = e.UniqueSlug()
|
||||||
|
e.Content = "= " + e.Title
|
||||||
|
return &e
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetBySlug(db *db.Database, slug string) (Entry, error) {
|
func GetBySlug(db *db.Database, slug string) (Entry, error) {
|
||||||
|
@ -66,6 +110,7 @@ func GetBySlug(db *db.Database, slug string) (Entry, error) {
|
||||||
if err := db.Get(&e, q, slug); err != nil {
|
if err := db.Get(&e, q, slug); err != nil {
|
||||||
return e, err
|
return e, err
|
||||||
}
|
}
|
||||||
|
e.Title = e.GenerateTitle()
|
||||||
return e, e.populateTags()
|
return e, e.populateTags()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,28 +120,53 @@ func GetByID(db *db.Database, id int64) (Entry, error) {
|
||||||
if err := db.Get(&e, q, id); err != nil {
|
if err := db.Get(&e, q, id); err != nil {
|
||||||
return e, err
|
return e, err
|
||||||
}
|
}
|
||||||
|
e.Title = e.GenerateTitle()
|
||||||
return e, e.populateTags()
|
return e, e.populateTags()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Search(db *db.Database, query string) ([]*Entry, error) {
|
func SearchByTag(db *db.Database, query string, tags []string) ([]*Entry, error) {
|
||||||
entries := []*Entry{}
|
entries := []*Entry{}
|
||||||
if query != "" {
|
query = fmt.Sprintf("%%%s%%", query)
|
||||||
q := `select * from entries where content like '%?%'`
|
log.Debug().Str("tag query", query).Int("len(tags)", len(tags)).Msg("searching")
|
||||||
err := db.Select(&entries, q, query)
|
|
||||||
|
if len(tags) > 0 {
|
||||||
|
q := `select e.*
|
||||||
|
from entries e
|
||||||
|
inner join tags t
|
||||||
|
on e.id=t.entry_id
|
||||||
|
where
|
||||||
|
t.name in (?)
|
||||||
|
AND content like ?
|
||||||
|
order by updated desc`
|
||||||
|
|
||||||
|
q, args, err := sqlx.In(q, tags, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Select(&entries, q, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
q := `select * from entries`
|
q := `select e.*
|
||||||
err := db.Select(&entries, q)
|
from entries e
|
||||||
|
where
|
||||||
|
content like ?
|
||||||
|
order by updated desc`
|
||||||
|
|
||||||
|
err := db.Select(&entries, q, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
e.db = db
|
e.db = db
|
||||||
|
e.Title = e.GenerateTitle()
|
||||||
e.populateTags()
|
e.populateTags()
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,12 +184,12 @@ func RemoveBySlug(db *db.Database, slug string) error {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
q = `delete from entries where entry_id = ?`
|
q = `delete from entries where id = ?`
|
||||||
if _, err := tx.Exec(q, e.ID); err != nil {
|
if _, err := tx.Exec(q, e.ID); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Entry) populateTags() error {
|
func (e *Entry) populateTags() error {
|
||||||
|
@ -141,18 +211,16 @@ func (e *Entry) removeTag(tag string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Entry) UniqueSlug() string {
|
func (e *Entry) GenerateTitle() string {
|
||||||
candidate := strings.Split(e.Content, "\n")[0]
|
candidate := strings.Split(e.Content, "\n")[0]
|
||||||
candidateNumber := 0
|
candidateNumber := 0
|
||||||
|
|
||||||
r := regexp.MustCompile(`[^a-zA-Z0-9 -]`)
|
r := regexp.MustCompile(`[^a-zA-Z0-9 -]`)
|
||||||
candidate = r.ReplaceAllString(candidate, "")
|
candidate = r.ReplaceAllString(candidate, "")
|
||||||
candidate = strings.TrimSpace(candidate)
|
candidate = strings.TrimSpace(candidate)
|
||||||
candidate = strings.ReplaceAll(candidate, " ", "-")
|
|
||||||
if len(candidate) == 0 {
|
if len(candidate) == 0 {
|
||||||
candidate = "untitled"
|
candidate = "untitled"
|
||||||
}
|
}
|
||||||
candidate = strings.ToLower(candidate)
|
|
||||||
|
|
||||||
q := `select slug from entries where slug like ?`
|
q := `select slug from entries where slug like ?`
|
||||||
slugs := []string{}
|
slugs := []string{}
|
||||||
|
@ -179,6 +247,20 @@ func (e *Entry) UniqueSlug() string {
|
||||||
return tmpCandidate
|
return tmpCandidate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Entry) UniqueSlug() string {
|
||||||
|
if e.Title == "" {
|
||||||
|
e.Title = e.GenerateTitle()
|
||||||
|
}
|
||||||
|
candidate := e.Title
|
||||||
|
|
||||||
|
r := regexp.MustCompile(`[^a-zA-Z0-9 -]`)
|
||||||
|
candidate = r.ReplaceAllString(candidate, "")
|
||||||
|
candidate = strings.TrimSpace(candidate)
|
||||||
|
candidate = strings.ReplaceAll(candidate, " ", "-")
|
||||||
|
candidate = strings.ToLower(candidate)
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Entry) Update() error {
|
func (e *Entry) Update() error {
|
||||||
if e.ID == -1 {
|
if e.ID == -1 {
|
||||||
return e.Create()
|
return e.Create()
|
||||||
|
@ -247,3 +329,12 @@ func (e *Entry) Create() error {
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Entry) HasTag(tag string) bool {
|
||||||
|
for _, t := range e.Tags {
|
||||||
|
if strings.ToLower(tag) == strings.ToLower(t) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
//go:generate esc -prefix frontend/dist -o esc.go frontend/dist
|
||||||
|
package main
|
|
@ -8,14 +8,19 @@
|
||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vue/cli": "^4.0.5",
|
||||||
"asciidoctor": "^2.0.3",
|
"asciidoctor": "^2.0.3",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"bootstrap": "^4.3.1",
|
"bootstrap": "^4.3.1",
|
||||||
"bootstrap-vue": "^2.0.4",
|
"bootstrap-vue": "^2.0.4",
|
||||||
|
"bootswatch": "^4.3.1",
|
||||||
"brace": "latest",
|
"brace": "latest",
|
||||||
"core-js": "^3.3.2",
|
"core-js": "^3.3.2",
|
||||||
|
"jquery": "^1.9.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
"popper.js": "^1.14.7",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
|
"vue-cookies": "^1.7.0",
|
||||||
"vue-router": "^3.1.3",
|
"vue-router": "^3.1.3",
|
||||||
"vue2-ace-editor": "^0.0.15",
|
"vue2-ace-editor": "^0.0.15",
|
||||||
"vuex": "^3.0.1"
|
"vuex": "^3.0.1"
|
||||||
|
@ -29,6 +34,9 @@
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-plugin-vue": "^5.0.0",
|
"eslint-plugin-vue": "^5.0.0",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"sass": "^1.23.0",
|
||||||
|
"sass-loader": "^8.0.0",
|
||||||
|
"vue-template-compiler": "^2.6.10",
|
||||||
|
"webpack": "^4.36.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
<title>frontend</title>
|
<title>Cabinet</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|
|
@ -1,37 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<b-navbar type="dark" variant="dark">
|
<b-navbar type="dark" variant="primary" class="navbar">
|
||||||
<b-navbar-brand>🗄 Cabinet</b-navbar-brand>
|
<b-navbar-brand>🗄 Cabinet</b-navbar-brand>
|
||||||
<b-navbar-nav>
|
<b-navbar-nav>
|
||||||
<b-nav-item to="/">Home</b-nav-item>
|
<b-nav-item to="/">Home</b-nav-item>
|
||||||
<b-nav-item to="/console">Console</b-nav-item>
|
<b-nav-item to="/console">Console</b-nav-item>
|
||||||
<b-nav-item to="/about">About</b-nav-item>
|
<b-nav-item to="/about">About</b-nav-item>
|
||||||
</b-navbar-nav>
|
</b-navbar-nav>
|
||||||
|
<b-navbar-nav class="ml-auto">
|
||||||
|
<b-nav-form>
|
||||||
|
<b-button @click="newFile">+</b-button>
|
||||||
|
</b-nav-form>
|
||||||
|
</b-navbar-nav>
|
||||||
</b-navbar>
|
</b-navbar>
|
||||||
<Error/>
|
<Error/>
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
#app {
|
.navbar {
|
||||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
padding: 0.5em !important;
|
||||||
-webkit-font-smoothing: antialiased;
|
margin-bottom: 1em;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav a {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav a.router-link-exact-active {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -42,6 +32,25 @@
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: {
|
components: {
|
||||||
Error
|
Error
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (!this.$store.state.key) {
|
||||||
|
let key = this.$cookies.get('key')
|
||||||
|
if (key) {
|
||||||
|
this.$store.commit('setKey', key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
newFile: function() {
|
||||||
|
this.$store.dispatch('newFile')
|
||||||
|
.catch(() => {})
|
||||||
|
.then(file => {
|
||||||
|
this.$store.dispatch('updateSearch')
|
||||||
|
this.$router.push({ name: 'console-slug', params: { slug: file.Slug }})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<editor ref="myEditor" v-model="text" @init="editorInit" lang="asciidoc" theme="github" width="100%" height="500" />
|
<editor
|
||||||
|
ref="myEditor"
|
||||||
|
v-model="text"
|
||||||
|
@init="editorInit"
|
||||||
|
lang="asciidoc"
|
||||||
|
theme="tomorrow_night"
|
||||||
|
width="100%"
|
||||||
|
height="500" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -20,7 +27,6 @@
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
content: function(newValue) {
|
content: function(newValue) {
|
||||||
console.log('text update\n'+newValue)
|
|
||||||
let editor = this.$refs.myEditor.editor
|
let editor = this.$refs.myEditor.editor
|
||||||
editor.renderer.updateFull()
|
editor.renderer.updateFull()
|
||||||
this.text = newValue
|
this.text = newValue
|
||||||
|
@ -33,7 +39,7 @@
|
||||||
editorInit: function () {
|
editorInit: function () {
|
||||||
require('brace/ext/language_tools') //language extension prerequsite...
|
require('brace/ext/language_tools') //language extension prerequsite...
|
||||||
require('brace/mode/asciidoc') //language
|
require('brace/mode/asciidoc') //language
|
||||||
require('brace/theme/github')
|
require('brace/theme/tomorrow_night')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<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})
|
||||||
|
.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>
|
|
@ -56,32 +56,29 @@
|
||||||
this.$store.dispatch('getFile', slug)
|
this.$store.dispatch('getFile', slug)
|
||||||
},
|
},
|
||||||
tagUpdate: function(newTags) {
|
tagUpdate: function(newTags) {
|
||||||
if (JSON.stringify(newTags) === JSON.stringify(this.file.Tags))
|
if (JSON.stringify(newTags) === JSON.stringify(this.$store.state.file.Tags))
|
||||||
return
|
return
|
||||||
this.file.Tags = newTags
|
this.$store.commit('setTags', newTags)
|
||||||
this.$emit('markDirty', true)
|
this.$emit('markDirty', true)
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
updateContent: function (newContent) {
|
updateContent: function (newContent) {
|
||||||
if (this.$store.state.file.Content === newContent)
|
if (this.$store.state.file.Content === newContent)
|
||||||
return
|
return
|
||||||
this.$store.state.file.Content = newContent
|
this.$store.commit('setContent', newContent)
|
||||||
this.$emit('markDirty', true)
|
this.$emit('markDirty', true)
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
save: function () {
|
save: function () {
|
||||||
this.$store.state.file.Content = this.content
|
this.$store.commit('setContent', this.content)
|
||||||
console.log("Saving file: " + this.file.Slug)
|
this.$store.dispatch('saveFile')
|
||||||
this.$store.dispatch('saveFile', this.file)
|
|
||||||
.then(res => {
|
.then(res => {
|
||||||
this.$emit('markDirty', false)
|
this.$emit('markDirty', false)
|
||||||
this.$store.dispatch('updateSearch')
|
this.$store.dispatch('updateSearch')
|
||||||
if (res.data.Slug != this.$route.params.slug)
|
if (res.data.Slug !== this.$route.params.slug)
|
||||||
this.$router.replace({params: { slug: res.data.Slug }})
|
this.$router.replace({params: { slug: res.data.Slug }})
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(() => { })
|
||||||
console.log('err:' + err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {TagList, Editor}
|
components: {TagList, Editor}
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div :hidden="!content">
|
<div :hidden="!content">
|
||||||
<b-container fluid>
|
|
||||||
<b-row>
|
|
||||||
<b-col>
|
|
||||||
<Viewer :content="content" />
|
<Viewer :content="content" />
|
||||||
</b-col>
|
<b-card-group :hidden="!tags">
|
||||||
</b-row>
|
<b-card
|
||||||
<b-row>
|
style="max-width: 50%"
|
||||||
<b-col cols="10">
|
header="Tags"
|
||||||
<label for="tagList" >Tags</label>
|
header-tag="header"
|
||||||
</b-col>
|
>
|
||||||
</b-row>
|
|
||||||
<b-row>
|
|
||||||
<b-col cols="10">
|
|
||||||
<TagList id="tagList" :tags="tags" :readOnly="true" />
|
<TagList id="tagList" :tags="tags" :readOnly="true" />
|
||||||
</b-col>
|
</b-card>
|
||||||
</b-row>
|
</b-card-group>
|
||||||
</b-container>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<b-container fluid>
|
<b-container fluid>
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-input placeholder="Search" @update="runQuery" v-model="queryText" />
|
<b-col class="searchBox">
|
||||||
|
<b-input placeholder="Search" @update="getResults" v-model="queryText" />
|
||||||
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
<b-row v-for="item in results" v-bind:key="item.ID">
|
<b-row v-for="item in results" v-bind:key="item.ID">
|
||||||
<b-col>
|
<b-col>
|
||||||
|
<b-button :hidden="!editMode" size="sm" class="deleteLink" @click="deleteFile(item.Slug)">X</b-button>
|
||||||
<b-link
|
<b-link
|
||||||
|
class="searchLink"
|
||||||
:to="{ name: target, params: { slug: item.Slug } }"
|
:to="{ name: target, params: { slug: item.Slug } }"
|
||||||
>{{item.Slug}}</b-link>
|
>{{item.Title}}</b-link>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-container>
|
</b-container>
|
||||||
|
@ -24,30 +28,46 @@
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
target: String,
|
target: String,
|
||||||
query: String
|
query: String,
|
||||||
|
editMode: Boolean
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
query: function(newValue) {
|
query: function(newValue) {
|
||||||
this.queryText = newValue
|
this.queryText = newValue
|
||||||
|
},
|
||||||
|
queryText: function(newValue) {
|
||||||
|
this.$store.commit('setQuery', newValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
results: function() {
|
results: function() {
|
||||||
console.log("results:"+this.$store.state.searchResults)
|
|
||||||
return this.$store.state.searchResults
|
return this.$store.state.searchResults
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.getResults()
|
this.getResults()
|
||||||
this.runQuery = _.debounce(this.runQuery, 1000)
|
this.getResults = _.debounce(this.getResults, 1000)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getResults: function () {
|
getResults: function () {
|
||||||
this.$store.dispatch('getSearchResults', null)
|
this.$store.dispatch('updateSearch')
|
||||||
},
|
},
|
||||||
runQuery: function() {
|
deleteFile: function(slug) {
|
||||||
this.$store.dispatch('getSearchResults', this.query)
|
this.$store.dispatch('deleteBySlug', slug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.deleteLink {
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
||||||
|
.searchBox {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
.searchLink {
|
||||||
|
margin-left: 1em;
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
tags: function (newValue) {
|
tags: function (newValue) {
|
||||||
this.internalTags = newValue
|
this.internalTags = newValue || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -4,13 +4,19 @@ import router from './router'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
|
||||||
import BootstrapVue from 'bootstrap-vue'
|
import BootstrapVue from 'bootstrap-vue'
|
||||||
|
import VueCookies from 'vue-cookies'
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.css'
|
// import "bootswatch/dist/darkly/variables";
|
||||||
|
// import "bootstrap/scss/bootstrap";
|
||||||
|
import "bootswatch/dist/darkly/bootstrap.css";
|
||||||
|
|
||||||
|
// import 'bootstrap/dist/css/bootstrap.css'
|
||||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
Vue.use(VueCookies)
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import Home from '../views/Home.vue'
|
import Home from '../views/Home.vue'
|
||||||
|
import Login from '../views/Login.vue'
|
||||||
import Console from '../views/Console.vue'
|
import Console from '../views/Console.vue'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
@ -31,6 +32,11 @@ const routes = [
|
||||||
name: 'console',
|
name: 'console',
|
||||||
component: Console
|
component: Console
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: Login
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/about',
|
path: '/about',
|
||||||
name: 'about',
|
name: 'about',
|
||||||
|
|
|
@ -23,7 +23,8 @@ export default new Vuex.Store({
|
||||||
errs: [],
|
errs: [],
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
query: null,
|
query: null,
|
||||||
file: null
|
file: null,
|
||||||
|
key: null
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
clearError(state) {
|
clearError(state) {
|
||||||
|
@ -40,9 +41,35 @@ export default new Vuex.Store({
|
||||||
},
|
},
|
||||||
setFile(state, file) {
|
setFile(state, file) {
|
||||||
state.file = file
|
state.file = file
|
||||||
|
},
|
||||||
|
setContent(state, content) {
|
||||||
|
state.file.Content = content
|
||||||
|
},
|
||||||
|
setTags(state, tags) {
|
||||||
|
state.file.Tags = tags
|
||||||
|
},
|
||||||
|
setKey(state, key) {
|
||||||
|
axios.defaults.headers.common['X-Auth-Key'] = key
|
||||||
|
state.key = key
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
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)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
commit('setFile', res.data)
|
||||||
|
resolve(res.data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
getFile: function ({commit}, slug) {
|
getFile: function ({commit}, slug) {
|
||||||
if (slug)
|
if (slug)
|
||||||
return axios.get('/v1/entries/' + slug)
|
return axios.get('/v1/entries/' + slug)
|
||||||
|
@ -51,31 +78,51 @@ export default new Vuex.Store({
|
||||||
commit('setFile', res.data)
|
commit('setFile', res.data)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getSearchResults: function ({dispatch, commit}, query) {
|
|
||||||
commit('setQuery', query)
|
|
||||||
dispatch('updateSearch')
|
|
||||||
},
|
|
||||||
updateSearch: function ({commit, state}) {
|
updateSearch: function ({commit, state}) {
|
||||||
let query = state.query
|
return new Promise((resolve, reject) => {
|
||||||
if (query) {
|
let query = state.query || ""
|
||||||
axios.get('/v1/entries?query=' + query)
|
axios.get('/v1/entries?query=' + query)
|
||||||
.catch(err => state.addError(err))
|
.catch(err => {
|
||||||
.then(res =>{
|
state.addError(err)
|
||||||
console.log("getSearchResults:"+res.data)
|
reject(err)
|
||||||
commit('setResults', res.data)
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
axios.get('/v1/entries')
|
|
||||||
.catch(err => state.addError(err))
|
|
||||||
.then(res => {
|
.then(res => {
|
||||||
console.log("getSearchResults:"+res.data)
|
|
||||||
commit('setResults', res.data)
|
commit('setResults', res.data)
|
||||||
|
resolve(res.data)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveFile: function(state, file) {
|
saveFile: function ({state}) {
|
||||||
return axios.put('/v1/entries/'+file.Slug, file)
|
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, state}, slug) {
|
||||||
|
if (!state.key)
|
||||||
|
return new Promise((resolve, reject) => { reject('not logged in') })
|
||||||
|
axios.delete('/v1/entries/' + slug)
|
||||||
|
.catch(err => {
|
||||||
|
commit('addError', err)
|
||||||
|
})
|
||||||
|
.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: {}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,13 +2,22 @@
|
||||||
<b-container fluid>
|
<b-container fluid>
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col md="5">
|
<b-col md="5">
|
||||||
<h2>Scratchpad</h2>
|
<div>
|
||||||
|
<b-tabs content-class="mt-3">
|
||||||
|
<b-tab active>
|
||||||
|
<template v-slot:title>
|
||||||
|
Scratchpad
|
||||||
|
</template>
|
||||||
<ScratchPad />
|
<ScratchPad />
|
||||||
|
</b-tab>
|
||||||
|
</b-tabs>
|
||||||
|
</div>
|
||||||
</b-col>
|
</b-col>
|
||||||
|
|
||||||
<b-col md="5">
|
<b-col md="5">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<b-tabs content-class="mt-3">
|
<b-tabs content-class="mt-3" v-model="tabIndex">
|
||||||
<b-tab active>
|
<b-tab active>
|
||||||
<template v-slot:title>
|
<template v-slot:title>
|
||||||
Editor <span v-bind:class="{ dirty: isDirty, clean: !isDirty }">•</span>
|
Editor <span v-bind:class="{ dirty: isDirty, clean: !isDirty }">•</span>
|
||||||
|
@ -24,8 +33,16 @@
|
||||||
|
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col md="2">
|
<b-col md="2">
|
||||||
<h2>Search Results</h2>
|
<div>
|
||||||
<SearchResults target="console-slug" :query="query"/>
|
<b-tabs content-class="mt-3">
|
||||||
|
<b-tab active>
|
||||||
|
<template v-slot:title>
|
||||||
|
Search
|
||||||
|
</template>
|
||||||
|
<SearchResults :editMode="true" target="console-slug" />
|
||||||
|
</b-tab>
|
||||||
|
</b-tabs>
|
||||||
|
</div>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
</b-container>
|
</b-container>
|
||||||
|
@ -49,15 +66,11 @@ export default {
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
query: ''
|
tabIndex: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
provide: {
|
|
||||||
update: function() {}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
markDirty: function(dirty) {
|
markDirty: function(dirty) {
|
||||||
console.log('markDirty:'+dirty)
|
|
||||||
this.isDirty = dirty
|
this.isDirty = dirty
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -65,8 +78,11 @@ export default {
|
||||||
// called before the route that renders this component is confirmed.
|
// called before the route that renders this component is confirmed.
|
||||||
// does NOT have access to `this` component instance,
|
// does NOT have access to `this` component instance,
|
||||||
// because it has not been created yet when this guard is called!
|
// because it has not been created yet when this guard is called!
|
||||||
console.log('beforeRouteEnter'+to+from)
|
next(vm => {
|
||||||
next()
|
if (!vm.$store.state.key) {
|
||||||
|
vm.$router.push({name: "login", params: {returnTo: vm.$route.path}})
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
beforeRouteUpdate (to, from, next) {
|
beforeRouteUpdate (to, from, next) {
|
||||||
// called when the route that renders this component has changed,
|
// called when the route that renders this component has changed,
|
||||||
|
@ -83,13 +99,13 @@ export default {
|
||||||
next(false)
|
next(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.tabIndex = 0
|
||||||
next()
|
next()
|
||||||
},
|
},
|
||||||
beforeRouteLeave (to, from, next) {
|
beforeRouteLeave (to, from, next) {
|
||||||
// called when the route that renders this component is about to
|
// called when the route that renders this component is about to
|
||||||
// be navigated away from.
|
// be navigated away from.
|
||||||
// has access to `this` component instance.
|
// has access to `this` component instance.
|
||||||
console.log('beforeRouteLeave'+to+from)
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
6479
frontend/yarn.lock
6479
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
10
go.mod
10
go.mod
|
@ -3,14 +3,18 @@ module code.chrissexton.org/cws/cabinet
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gobuffalo/packr/v2 v2.7.1
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gorilla/handlers v1.4.2
|
github.com/gorilla/handlers v1.4.2
|
||||||
github.com/gorilla/mux v1.7.3
|
github.com/gorilla/mux v1.7.3
|
||||||
github.com/jmoiron/sqlx v1.2.0
|
github.com/jmoiron/sqlx v1.2.0
|
||||||
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.11.0
|
github.com/mattn/go-sqlite3 v1.11.0
|
||||||
github.com/rs/zerolog v1.16.0
|
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/graceful v1.2.15
|
||||||
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect
|
github.com/stretchr/testify v1.4.0
|
||||||
|
golang.org/x/crypto v0.0.0-20191107222254-f4817d981bb6
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect
|
||||||
google.golang.org/appengine v1.6.5 // indirect
|
google.golang.org/appengine v1.6.5 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.7 // indirect
|
||||||
)
|
)
|
||||||
|
|
75
go.sum
75
go.sum
|
@ -1,87 +1,64 @@
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
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.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/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/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/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
|
|
||||||
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
|
|
||||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
|
||||||
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/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/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/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/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
|
||||||
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
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/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/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/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.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/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
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/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/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/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
|
||||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
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/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/stretchr/graceful v1.2.15 h1:vmXbwPGfe8bI6KkgmHry/P1Pk63bM3TDcfi+5mh+VHg=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
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=
|
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
|
||||||
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/go.mod h1:IxdGAOTZueMKoBr3oJIzdeg5CCCXbHXfV44sLhfAXXI=
|
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.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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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=
|
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
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-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-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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
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=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
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.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/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74 h1:4cFkmztxtMslUX2SctSl+blCyXfpzhGOy9LhKAqSMA4=
|
||||||
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
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=
|
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=
|
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 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/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=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||||
|
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|
24
main.go
24
main.go
|
@ -1,19 +1,18 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.chrissexton.org/cws/cabinet/entry"
|
|
||||||
"flag"
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"code.chrissexton.org/cws/cabinet/db"
|
|
||||||
"code.chrissexton.org/cws/cabinet/web"
|
|
||||||
|
|
||||||
packr "github.com/gobuffalo/packr/v2"
|
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"code.chrissexton.org/cws/cabinet/auth"
|
||||||
|
"code.chrissexton.org/cws/cabinet/db"
|
||||||
|
"code.chrissexton.org/cws/cabinet/entry"
|
||||||
|
"code.chrissexton.org/cws/cabinet/web"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -22,6 +21,7 @@ var (
|
||||||
salt = flag.String("salt", "c4b1n3t", "salt for IDs")
|
salt = flag.String("salt", "c4b1n3t", "salt for IDs")
|
||||||
minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size")
|
minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size")
|
||||||
develop = flag.Bool("develop", false, "turn on develop mode")
|
develop = flag.Bool("develop", false, "turn on develop mode")
|
||||||
|
adminSecret = flag.String("adminSecret", "helpme123", "secret for user creation")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -29,9 +29,6 @@ func main() {
|
||||||
log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
|
||||||
box := packr.New("dist", "frontend/dist")
|
|
||||||
log.Debug().Strs("dirlist", box.List()).Msg("packr made")
|
|
||||||
|
|
||||||
db, err := db.New(*dbPath)
|
db, err := db.New(*dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().
|
log.Fatal().
|
||||||
|
@ -43,8 +40,11 @@ func main() {
|
||||||
if err := entry.PrepareTable(tx); err != nil {
|
if err := entry.PrepareTable(tx); err != nil {
|
||||||
log.Fatal().Err(err).Msg("could not create database")
|
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()
|
tx.Commit()
|
||||||
|
|
||||||
s := web.New(*httpAddr, db, box)
|
s := web.New(*httpAddr, db, FS(*develop))
|
||||||
s.Serve()
|
s.Serve()
|
||||||
}
|
}
|
||||||
|
|
27
todo.adoc
27
todo.adoc
|
@ -1,27 +0,0 @@
|
||||||
= Todo
|
|
||||||
:icons: font
|
|
||||||
|
|
||||||
* Operations
|
|
||||||
** [ ] dockerize the build
|
|
||||||
** [ ] integrate CI/CD
|
|
||||||
** [ ] run on https://cabinet.chrissexton.org[cabinet.chrissexton.org]
|
|
||||||
** [ ] create redirect or https://cab.chrissexton.org[cab.chrissexton.org]
|
|
||||||
* Vue Frontend
|
|
||||||
** [ ] spend some time learning about TypeScript/Vue.js style
|
|
||||||
** Documents
|
|
||||||
*** [x] adoc editor widget
|
|
||||||
** Authentication
|
|
||||||
*** [ ] some kind of user auth
|
|
||||||
** Views
|
|
||||||
*** [ ] editor view
|
|
||||||
*** [ ] public index/search view
|
|
||||||
* Backend
|
|
||||||
** [?] save endpoint
|
|
||||||
*** [ ] need to generate a slug for entries
|
|
||||||
*** [ ] add authentication/authorization
|
|
||||||
*** [ ] convert document to adoc (give format?)
|
|
||||||
*** [x] test in frontend
|
|
||||||
*** [ ] check for unique tags
|
|
||||||
*** [ ] set some db fields not null
|
|
||||||
** [ ] search endpoint
|
|
||||||
* CLI Frontend
|
|
|
@ -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)
|
||||||
|
}
|
127
web/entry.go
127
web/entry.go
|
@ -3,6 +3,7 @@ package web
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -12,17 +13,32 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (web *Web) writeJSON(w http.ResponseWriter, code int, data interface{}) error {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
resp, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.WriteHeader(code)
|
||||||
|
w.Write(resp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (web *Web) editEntry(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) editEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
slug := vars["slug"]
|
slug := vars["slug"]
|
||||||
dec := json.NewDecoder(r.Body)
|
dec := json.NewDecoder(r.Body)
|
||||||
newEntry := entry.New(web.db)
|
req := struct {
|
||||||
err := dec.Decode(&newEntry)
|
Content string
|
||||||
|
}{}
|
||||||
|
err := dec.Decode(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
fmt.Fprint(w, err)
|
fmt.Fprint(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
newEntry := entry.NewFromMd(web.db, req.Content)
|
||||||
|
|
||||||
oldEntry, err := entry.GetBySlug(web.db, slug)
|
oldEntry, err := entry.GetBySlug(web.db, slug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -32,72 +48,94 @@ func (web *Web) editEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
oldEntry.Content = newEntry.Content
|
oldEntry.Content = newEntry.Content
|
||||||
|
oldEntry.Title = newEntry.Title
|
||||||
|
oldEntry.Slug = newEntry.UniqueSlug()
|
||||||
oldEntry.Tags = newEntry.Tags
|
oldEntry.Tags = newEntry.Tags
|
||||||
oldEntry.Updated = time.Now()
|
oldEntry.Updated = time.Now()
|
||||||
|
|
||||||
err = oldEntry.Update()
|
err = oldEntry.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
web.writeJSON(w, 500, err)
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := json.Marshal(oldEntry)
|
web.writeJSON(w, 200, oldEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) newAdocEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
web.writeJSON(w, 500, err)
|
||||||
fmt.Fprint(w, err)
|
return
|
||||||
|
}
|
||||||
|
newEntry := entry.NewFromAdoc(web.db, string(body))
|
||||||
|
err = newEntry.Create()
|
||||||
|
if err != nil {
|
||||||
|
web.writeJSON(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Interface("oldEntry", oldEntry).Msg("Got a PUT")
|
web.writeJSON(w, 200, newEntry)
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("content-type", "application/json")
|
func (web *Web) newMarkdownEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, string(resp))
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not read message body")
|
||||||
|
web.writeJSON(w, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newEntry := entry.NewFromMd(web.db, string(body))
|
||||||
|
err = newEntry.Create()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not create entry")
|
||||||
|
web.writeJSON(w, 500, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
web.writeJSON(w, 200, newEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) newEntry(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) newEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Debug().
|
||||||
|
Str("content-type", r.Header.Get("Content-Type")).
|
||||||
|
Msgf("newEntry")
|
||||||
dec := json.NewDecoder(r.Body)
|
dec := json.NewDecoder(r.Body)
|
||||||
newEntry := entry.New(web.db)
|
newEntry := entry.New(web.db)
|
||||||
err := dec.Decode(&newEntry)
|
err := dec.Decode(&newEntry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
log.Error().Err(err).Msgf("could not decode entry")
|
||||||
fmt.Fprint(w, err)
|
web.writeJSON(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = newEntry.Create()
|
err = newEntry.Create()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
log.Error().Err(err).Msgf("could not create raw entry")
|
||||||
fmt.Fprint(w, err)
|
web.writeJSON(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := json.Marshal(newEntry)
|
|
||||||
if err != nil {
|
web.writeJSON(w, 200, newEntry)
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("content-type", "application/json")
|
|
||||||
fmt.Fprint(w, string(resp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) allEntries(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) allEntries(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
query := ""
|
||||||
query := r.Form.Get("query")
|
tags := []string{}
|
||||||
entries, err := entry.Search(web.db, query)
|
if !web.AuthCheck(r) {
|
||||||
|
tags = append(tags, "public")
|
||||||
|
}
|
||||||
|
items, ok := r.URL.Query()["query"]
|
||||||
|
if ok {
|
||||||
|
query = items[0]
|
||||||
|
}
|
||||||
|
entries, err := entry.SearchByTag(web.db, query, tags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
log.Error().Msgf("Error querying: %w", err)
|
||||||
fmt.Fprint(w, err)
|
web.writeJSON(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := json.Marshal(entries)
|
web.writeJSON(w, 200, entries)
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("content-type", "application/json")
|
|
||||||
fmt.Fprint(w, string(resp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) getEntry(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) getEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -106,19 +144,16 @@ func (web *Web) getEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
entry, err := entry.GetBySlug(web.db, slug)
|
entry, err := entry.GetBySlug(web.db, slug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
web.writeJSON(w, 500, err)
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := json.Marshal(entry)
|
if !web.AuthCheck(r) && !entry.HasTag("public") {
|
||||||
if err != nil {
|
web.writeJSON(w, 401, "not authorized")
|
||||||
w.WriteHeader(500)
|
|
||||||
fmt.Fprint(w, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("content-type", "application/json")
|
|
||||||
fmt.Fprint(w, string(resp))
|
web.writeJSON(w, 200, entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) removeEntry(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) removeEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -127,9 +162,9 @@ func (web *Web) removeEntry(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
err := entry.RemoveBySlug(web.db, slug)
|
err := entry.RemoveBySlug(web.db, slug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
log.Error().Msgf("Error deleting: %s", err)
|
||||||
fmt.Fprint(w, err)
|
web.writeJSON(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(200)
|
web.writeJSON(w, 200, "success")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"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"
|
"code.chrissexton.org/cws/cabinet/db"
|
||||||
|
|
||||||
packr "github.com/gobuffalo/packr/v2"
|
|
||||||
|
|
||||||
"github.com/speps/go-hashids"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/stretchr/graceful"
|
"github.com/stretchr/graceful"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,15 +20,14 @@ type Web struct {
|
||||||
addr string
|
addr string
|
||||||
db *db.Database
|
db *db.Database
|
||||||
salt string
|
salt string
|
||||||
h *hashids.HashID
|
static http.FileSystem
|
||||||
box *packr.Box
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(addr string, db *db.Database, box *packr.Box) *Web {
|
func New(addr string, db *db.Database, static http.FileSystem) *Web {
|
||||||
w := &Web{
|
w := &Web{
|
||||||
addr: addr,
|
addr: addr,
|
||||||
db: db,
|
db: db,
|
||||||
box: box,
|
static: static,
|
||||||
}
|
}
|
||||||
if err := db.MakeDB(); err != nil {
|
if err := db.MakeDB(); err != nil {
|
||||||
log.Fatal().
|
log.Fatal().
|
||||||
|
@ -39,14 +37,71 @@ func New(addr string, db *db.Database, box *packr.Box) *Web {
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthMiddleware struct {
|
||||||
|
web *Web
|
||||||
|
db *db.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthMiddleware(web *Web) AuthMiddleware {
|
||||||
|
return AuthMiddleware{
|
||||||
|
web: web,
|
||||||
|
db: web.db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (aw *AuthMiddleware) Middleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if aw.web.AuthCheck(r) {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(401)
|
||||||
|
fmt.Fprint(w, "invalid login")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (web *Web) AuthCheck(r *http.Request) bool {
|
||||||
|
key := r.Header.Get("X-Auth-Key")
|
||||||
|
u, err := auth.GetByKey(web.db, key)
|
||||||
|
if key == "" || err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("This shit is authed to user %s!", u.Name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (web *Web) routeSetup() http.Handler {
|
func (web *Web) routeSetup() http.Handler {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
api := r.PathPrefix("/v1/").Subrouter()
|
api := r.PathPrefix("/v1/").Subrouter()
|
||||||
api.HandleFunc("/entries", web.allEntries).Methods(http.MethodGet)
|
auth := NewAuthMiddleware(web)
|
||||||
api.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost)
|
|
||||||
api.HandleFunc("/entries", web.removeEntry).Methods(http.MethodDelete)
|
authedApi := r.PathPrefix("/v1/").Subrouter()
|
||||||
api.HandleFunc("/entries/{slug}", web.editEntry).Methods(http.MethodPut)
|
authedApi.Use(auth.Middleware)
|
||||||
|
|
||||||
|
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Debug().Msg("test json")
|
||||||
|
}).Methods(http.MethodPost).HeadersRegexp("Content-Type", `application/json.*`)
|
||||||
|
|
||||||
|
api.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Debug().Msg("test markdown")
|
||||||
|
}).Methods(http.MethodPost).HeadersRegexp("Content-Type", `application/markdown.*`)
|
||||||
|
|
||||||
|
// 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 }'
|
||||||
|
|
||||||
|
authedApi.HandleFunc("/entries", web.newEntry).Methods(http.MethodPost).
|
||||||
|
HeadersRegexp("Content-Type", "application/(text|json).*")
|
||||||
|
authedApi.HandleFunc("/entries", web.newMarkdownEntry).Methods(http.MethodPost).
|
||||||
|
HeadersRegexp("Content-Type", "application/markdown")
|
||||||
|
authedApi.HandleFunc("/entries", web.newAdocEntry).Methods(http.MethodPost).
|
||||||
|
HeadersRegexp("Content-Type", "application/asciidoc")
|
||||||
|
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/{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"))
|
r.PathPrefix("/").HandlerFunc(web.indexHandler("/index.html"))
|
||||||
loggedRouter := handlers.LoggingHandler(os.Stdout, r)
|
loggedRouter := handlers.LoggingHandler(os.Stdout, r)
|
||||||
return loggedRouter
|
return loggedRouter
|
||||||
|
|
|
@ -1,54 +1,60 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (web *Web) indexHandler(entryPoint string) func(w http.ResponseWriter, r *http.Request) {
|
func (web *Web) indexHandler(entryPoint string) func(w http.ResponseWriter, r *http.Request) {
|
||||||
entryPoint = path.Clean(strings.TrimPrefix(entryPoint, "/"))
|
entryPoint = path.Join(path.Clean(entryPoint))
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
p := path.Clean(strings.TrimPrefix(r.URL.Path, "/"))
|
p := path.Join(path.Clean(r.URL.Path))
|
||||||
log.Debug().Str("path", p).Msg("requested path")
|
log.Debug().Str("path", p).Msg("requested path")
|
||||||
|
|
||||||
if web.box.Has(p) && !web.box.HasDir(p) {
|
var info os.FileInfo
|
||||||
f, err := web.box.Find(p)
|
f, err := web.static.Open(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Error finding file")
|
log.Error().Err(err).Msg("Error finding file")
|
||||||
w.WriteHeader(http.StatusNotFound)
|
goto entryPoint
|
||||||
}
|
}
|
||||||
write(w, f, p)
|
defer f.Close()
|
||||||
return
|
info, err = f.Stat()
|
||||||
}
|
|
||||||
if web.box.HasDir(p) && web.box.Has(path.Join(p, "index.html")) {
|
|
||||||
f, err := web.box.Find(path.Join(p, "index.html"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Error finding file")
|
log.Error().Err(err).Msg("Error finding file")
|
||||||
w.WriteHeader(http.StatusNotFound)
|
goto entryPoint
|
||||||
}
|
}
|
||||||
write(w, f, p)
|
if info.IsDir() {
|
||||||
return
|
if f, err := web.static.Open(path.Join(p, "index.html")); err == nil && f != nil {
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("path", p).Str("entry", entryPoint).Msg("all handlers fell through, giving default")
|
|
||||||
if f, err := web.box.Find(entryPoint); err == nil {
|
|
||||||
write(w, f, p)
|
write(w, f, p)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
log.Error().AnErr("err", err).Msgf("could not load any files %s", err)
|
log.Error().Msgf("Could not load file %s: %w", path.Join(p, "index.html"), err)
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
write(w, f, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entryPoint:
|
||||||
|
if f, err := web.static.Open(entryPoint); err == nil {
|
||||||
|
write(w, f, p)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
log.Error().Msgf("Could not load file %s or %s", p, entryPoint)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
return fn
|
return fn
|
||||||
}
|
}
|
||||||
|
|
||||||
func write(w http.ResponseWriter, f []byte, path string) {
|
func write(w http.ResponseWriter, file http.File, path string) {
|
||||||
|
f, _ := ioutil.ReadAll(file)
|
||||||
ctype := mime.TypeByExtension(filepath.Ext(path))
|
ctype := mime.TypeByExtension(filepath.Ext(path))
|
||||||
if ctype == "" {
|
if ctype == "" {
|
||||||
ctype = http.DetectContentType(f)
|
ctype = http.DetectContentType(f)
|
||||||
|
|
Loading…
Reference in New Issue