initial bits for cromp

This commit is contained in:
Aaron Bieber 2020-03-31 07:38:45 -06:00
commit ee707eea82
12 changed files with 1000 additions and 0 deletions

73
cmd/cromp/main.go Normal file
View File

@ -0,0 +1,73 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"text/tabwriter"
"github.com/peterbourgon/ff/v2/ffcli"
cromp "suah.dev/cromp/internal"
)
func main() {
var (
rootFlagSet = flag.NewFlagSet("cromp", flag.ExitOnError)
loadFlagSet = flag.NewFlagSet("cromp load", flag.ExitOnError)
configFlagSet = flag.NewFlagSet("cromp config", flag.ExitOnError)
//createFlagSet = flag.NewFlagSet("cromp create", flag.ExitOnError)
)
root := &ffcli.Command{
ShortUsage: "cromp <subcommand>",
FlagSet: rootFlagSet,
Subcommands: []*ffcli.Command{
&ffcli.Command{
Name: "config",
ShortUsage: "cromp config key=value",
FlagSet: configFlagSet,
Exec: func(ctx context.Context, args []string) error {
if len(args) < 1 {
return fmt.Errorf("")
}
return nil
},
},
&ffcli.Command{
Name: "load",
ShortUsage: "cromp load <file>",
FlagSet: loadFlagSet,
Exec: func(ctx context.Context, args []string) error {
if len(args) != 1 {
return fmt.Errorf("missing file name")
}
header, err := cromp.ParseFileHeader(args[0])
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
fmt.Fprintf(w, "Title:\t%s\n", header.Title)
fmt.Fprintf(w, "Author:\t%s\n", header.Author)
fmt.Fprintf(w, "Date:\t%s\n", header.Date)
fmt.Fprintf(w, "UUID:\t%s\n", header.UUID)
w.Flush()
return nil
},
},
},
Exec: func(context.Context, []string) error {
return flag.ErrHelp
},
}
if err := root.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}

65
cmd/cromps/handlers.go Normal file
View File

@ -0,0 +1,65 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"suah.dev/cromp/db"
)
// NewUser is the handler for /user/new
func NewUser(w http.ResponseWriter, r *http.Request) {
var u db.CreateUserParams
json.NewDecoder(r.Body).Decode(&u)
user, err := base.CreateUser(ctx, u)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
// Auth is the handler for /usr/auth
func Auth(w http.ResponseWriter, r *http.Request) {
var p db.AuthUserParams
json.NewDecoder(r.Body).Decode(&p)
user, err := base.AuthUser(ctx, p)
if err != nil {
panic(err)
}
if user.Authed {
authedUsers[user.Token.String()] = user
// TODO respond with token
json.NewEncoder(w).Encode(user)
}
}
// AddEntry handles requests to /entries/add
func AddEntry(w http.ResponseWriter, r *http.Request) {
var e db.CreateEntryParams
json.NewDecoder(r.Body).Decode(&e)
entry, err := base.CreateEntry(ctx, e)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(entry)
}
// Entries handles requests to /entries
func Entries(w http.ResponseWriter, r *http.Request) {
// Print secret message
fmt.Fprintln(w, "The cake is a lie!")
}
// SimilarEntries
func SimilarEntries(w http.ResponseWriter, r *http.Request) {
}

91
cmd/cromps/main.go Normal file
View File

@ -0,0 +1,91 @@
package main
import (
"context"
"database/sql"
"log"
"net/http"
"time"
"github.com/google/uuid"
_ "github.com/lib/pq"
"suah.dev/cromp/db"
)
var (
pd, err = sql.Open("postgres", "host=localhost dbname=qbit sslmode=disable password=''")
ctx, cancel = context.WithCancel(context.Background())
base = db.New(pd)
authedUsers = make(map[string]db.AuthUserRow)
)
func logger(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s\n", r.URL.Path)
f(w, r)
}
}
func checkAuth(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Access-Token")
if token == "" {
log.Printf("checkAuth: %s received empty token\n", r.URL.Path)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if user, ok := authedUsers[token]; ok {
if time.Now().Before(user.TokenExpires) {
log.Printf("checkAuth: %s received valid token\n", r.URL.Path)
f(w, r)
} else {
delete(authedUsers, token)
log.Printf("checkAuth: %s received expired token\n", r.URL.Path)
http.Error(w, "Token Expired", http.StatusUnauthorized)
return
}
} else {
t, err := uuid.Parse(token)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user, err := base.GetUserByToken(ctx, t)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if time.Now().Before(user.TokenExpires) {
log.Printf("checkAuth: %s received valid token\n", r.URL.Path)
f(w, r)
} else {
delete(authedUsers, token)
log.Printf("checkAuth: %s received expired token\n", r.URL.Path)
http.Error(w, "Token Expired", http.StatusUnauthorized)
return
}
}
}
}
func main() {
defer cancel() // cancel when we are finished consuming integers
if err != nil {
panic(err)
}
defer pd.Close()
http.HandleFunc("/user/new", logger(NewUser))
http.HandleFunc("/user/auth", logger(Auth))
http.HandleFunc("/entries/add", checkAuth(logger(AddEntry)))
http.HandleFunc("/entries/delete", checkAuth(logger(Entries)))
http.HandleFunc("/entries/get", checkAuth(logger(Entries)))
http.HandleFunc("/entries/update", checkAuth(logger(Entries)))
http.HandleFunc("/entries/similar", checkAuth(logger(SimilarEntries)))
log.Fatalln(http.ListenAndServe(":8080", nil))
}

196
db/db.go Normal file
View File

@ -0,0 +1,196 @@
// Code generated by sqlc. DO NOT EDIT.
package db
import (
"context"
"database/sql"
"fmt"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
q := Queries{db: db}
var err error
if q.authUserStmt, err = db.PrepareContext(ctx, authUser); err != nil {
return nil, fmt.Errorf("error preparing query AuthUser: %w", err)
}
if q.createEntryStmt, err = db.PrepareContext(ctx, createEntry); err != nil {
return nil, fmt.Errorf("error preparing query CreateEntry: %w", err)
}
if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil {
return nil, fmt.Errorf("error preparing query CreateUser: %w", err)
}
if q.deleteEntryStmt, err = db.PrepareContext(ctx, deleteEntry); err != nil {
return nil, fmt.Errorf("error preparing query DeleteEntry: %w", err)
}
if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil {
return nil, fmt.Errorf("error preparing query DeleteUser: %w", err)
}
if q.entriesByTokenStmt, err = db.PrepareContext(ctx, entriesByToken); err != nil {
return nil, fmt.Errorf("error preparing query EntriesByToken: %w", err)
}
if q.getEntriesStmt, err = db.PrepareContext(ctx, getEntries); err != nil {
return nil, fmt.Errorf("error preparing query GetEntries: %w", err)
}
if q.getEntryStmt, err = db.PrepareContext(ctx, getEntry); err != nil {
return nil, fmt.Errorf("error preparing query GetEntry: %w", err)
}
if q.getUserStmt, err = db.PrepareContext(ctx, getUser); err != nil {
return nil, fmt.Errorf("error preparing query GetUser: %w", err)
}
if q.getUserByTokenStmt, err = db.PrepareContext(ctx, getUserByToken); err != nil {
return nil, fmt.Errorf("error preparing query GetUserByToken: %w", err)
}
if q.similarEntriesStmt, err = db.PrepareContext(ctx, similarEntries); err != nil {
return nil, fmt.Errorf("error preparing query SimilarEntries: %w", err)
}
if q.validTokenStmt, err = db.PrepareContext(ctx, validToken); err != nil {
return nil, fmt.Errorf("error preparing query ValidToken: %w", err)
}
return &q, nil
}
func (q *Queries) Close() error {
var err error
if q.authUserStmt != nil {
if cerr := q.authUserStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing authUserStmt: %w", cerr)
}
}
if q.createEntryStmt != nil {
if cerr := q.createEntryStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createEntryStmt: %w", cerr)
}
}
if q.createUserStmt != nil {
if cerr := q.createUserStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createUserStmt: %w", cerr)
}
}
if q.deleteEntryStmt != nil {
if cerr := q.deleteEntryStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteEntryStmt: %w", cerr)
}
}
if q.deleteUserStmt != nil {
if cerr := q.deleteUserStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteUserStmt: %w", cerr)
}
}
if q.entriesByTokenStmt != nil {
if cerr := q.entriesByTokenStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing entriesByTokenStmt: %w", cerr)
}
}
if q.getEntriesStmt != nil {
if cerr := q.getEntriesStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getEntriesStmt: %w", cerr)
}
}
if q.getEntryStmt != nil {
if cerr := q.getEntryStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getEntryStmt: %w", cerr)
}
}
if q.getUserStmt != nil {
if cerr := q.getUserStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getUserStmt: %w", cerr)
}
}
if q.getUserByTokenStmt != nil {
if cerr := q.getUserByTokenStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getUserByTokenStmt: %w", cerr)
}
}
if q.similarEntriesStmt != nil {
if cerr := q.similarEntriesStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing similarEntriesStmt: %w", cerr)
}
}
if q.validTokenStmt != nil {
if cerr := q.validTokenStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing validTokenStmt: %w", cerr)
}
}
return err
}
func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...)
case stmt != nil:
return stmt.ExecContext(ctx, args...)
default:
return q.db.ExecContext(ctx, query, args...)
}
}
func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...)
case stmt != nil:
return stmt.QueryContext(ctx, args...)
default:
return q.db.QueryContext(ctx, query, args...)
}
}
func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...)
case stmt != nil:
return stmt.QueryRowContext(ctx, args...)
default:
return q.db.QueryRowContext(ctx, query, args...)
}
}
type Queries struct {
db DBTX
tx *sql.Tx
authUserStmt *sql.Stmt
createEntryStmt *sql.Stmt
createUserStmt *sql.Stmt
deleteEntryStmt *sql.Stmt
deleteUserStmt *sql.Stmt
entriesByTokenStmt *sql.Stmt
getEntriesStmt *sql.Stmt
getEntryStmt *sql.Stmt
getUserStmt *sql.Stmt
getUserByTokenStmt *sql.Stmt
similarEntriesStmt *sql.Stmt
validTokenStmt *sql.Stmt
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
tx: tx,
authUserStmt: q.authUserStmt,
createEntryStmt: q.createEntryStmt,
createUserStmt: q.createUserStmt,
deleteEntryStmt: q.deleteEntryStmt,
deleteUserStmt: q.deleteUserStmt,
entriesByTokenStmt: q.entriesByTokenStmt,
getEntriesStmt: q.getEntriesStmt,
getEntryStmt: q.getEntryStmt,
getUserStmt: q.getUserStmt,
getUserByTokenStmt: q.getUserByTokenStmt,
similarEntriesStmt: q.similarEntriesStmt,
validTokenStmt: q.validTokenStmt,
}
}

32
db/models.go Normal file
View File

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
package db
import (
"database/sql"
"time"
"github.com/google/uuid"
)
type Entry struct {
EntryID uuid.UUID `json:"entry_id"`
UserID int32 `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
Title string `json:"title"`
Body string `json:"body"`
}
type User struct {
UserID int64 `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt sql.NullTime `json:"updated_at"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
Hash string `json:"hash"`
Email string `json:"email"`
Token uuid.UUID `json:"token"`
TokenExpires time.Time `json:"token_expires"`
}

334
db/queries.sql.go Normal file
View File

@ -0,0 +1,334 @@
// Code generated by sqlc. DO NOT EDIT.
// source: queries.sql
package db
import (
"context"
"time"
"github.com/google/uuid"
)
const authUser = `-- name: AuthUser :one
UPDATE users
SET
token = DEFAULT,
token_expires = DEFAULT
WHERE
username = $2 and
(hash = crypt($1, hash)) = true
RETURNING user_id, created_at, first_name, last_name, username, email, token, token_expires, true as authed
`
type AuthUserParams struct {
Crypt interface{} `json:"crypt"`
Username string `json:"username"`
}
type AuthUserRow struct {
UserID int64 `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
Email string `json:"email"`
Token uuid.UUID `json:"token"`
TokenExpires time.Time `json:"token_expires"`
Authed bool `json:"authed"`
}
func (q *Queries) AuthUser(ctx context.Context, arg AuthUserParams) (AuthUserRow, error) {
row := q.queryRow(ctx, q.authUserStmt, authUser, arg.Crypt, arg.Username)
var i AuthUserRow
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.FirstName,
&i.LastName,
&i.Username,
&i.Email,
&i.Token,
&i.TokenExpires,
&i.Authed,
)
return i, err
}
const createEntry = `-- name: CreateEntry :one
INSERT INTO entries (
entry_id, user_id, title, body
) VALUES (
$1, $2, $3, $4
)
RETURNING entry_id, created_at, to_tsvector(body)
`
type CreateEntryParams struct {
EntryID uuid.UUID `json:"entry_id"`
UserID int32 `json:"user_id"`
Title string `json:"title"`
Body string `json:"body"`
}
type CreateEntryRow struct {
EntryID uuid.UUID `json:"entry_id"`
CreatedAt time.Time `json:"created_at"`
ToTsvector interface{} `json:"to_tsvector"`
}
func (q *Queries) CreateEntry(ctx context.Context, arg CreateEntryParams) (CreateEntryRow, error) {
row := q.queryRow(ctx, q.createEntryStmt, createEntry,
arg.EntryID,
arg.UserID,
arg.Title,
arg.Body,
)
var i CreateEntryRow
err := row.Scan(&i.EntryID, &i.CreatedAt, &i.ToTsvector)
return i, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (
first_name, last_name, username, email, hash
) VALUES (
$1, $2, $3, $4, hash($5)
)
RETURNING user_id, username, token, token_expires
`
type CreateUserParams struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
type CreateUserRow struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
Token uuid.UUID `json:"token"`
TokenExpires time.Time `json:"token_expires"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
row := q.queryRow(ctx, q.createUserStmt, createUser,
arg.FirstName,
arg.LastName,
arg.Username,
arg.Email,
arg.Password,
)
var i CreateUserRow
err := row.Scan(
&i.UserID,
&i.Username,
&i.Token,
&i.TokenExpires,
)
return i, err
}
const deleteEntry = `-- name: DeleteEntry :exec
DELETE FROM entries
WHERE entry_id = $1
`
func (q *Queries) DeleteEntry(ctx context.Context, entryID uuid.UUID) error {
_, err := q.exec(ctx, q.deleteEntryStmt, deleteEntry, entryID)
return err
}
const deleteUser = `-- name: DeleteUser :exec
DELETE FROM users CASCADE
WHERE user_id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, userID int64) error {
_, err := q.exec(ctx, q.deleteUserStmt, deleteUser, userID)
return err
}
const entriesByToken = `-- name: EntriesByToken :many
SELECT entry_id, user_id, created_at, updated_at, title, body FROM entries
WHERE user_id = (SELECT user_id FROM users WHERE token = $1 limit 1)
`
func (q *Queries) EntriesByToken(ctx context.Context, token uuid.UUID) ([]Entry, error) {
rows, err := q.query(ctx, q.entriesByTokenStmt, entriesByToken, token)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Entry
for rows.Next() {
var i Entry
if err := rows.Scan(
&i.EntryID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Title,
&i.Body,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEntries = `-- name: GetEntries :many
SELECT entry_id, user_id, created_at, updated_at, title, body FROM entries
WHERE user_id = $1
`
func (q *Queries) GetEntries(ctx context.Context, userID int32) ([]Entry, error) {
rows, err := q.query(ctx, q.getEntriesStmt, getEntries, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Entry
for rows.Next() {
var i Entry
if err := rows.Scan(
&i.EntryID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Title,
&i.Body,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEntry = `-- name: GetEntry :one
SELECT entry_id, user_id, created_at, updated_at, title, body FROM entries
WHERE entry_id = $1 LIMIT 1
`
func (q *Queries) GetEntry(ctx context.Context, entryID uuid.UUID) (Entry, error) {
row := q.queryRow(ctx, q.getEntryStmt, getEntry, entryID)
var i Entry
err := row.Scan(
&i.EntryID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Title,
&i.Body,
)
return i, err
}
const getUser = `-- name: GetUser :one
SELECT user_id, created_at, updated_at, first_name, last_name, username, hash, email, token, token_expires FROM users
WHERE user_id = $1 LIMIT 1
`
func (q *Queries) GetUser(ctx context.Context, userID int64) (User, error) {
row := q.queryRow(ctx, q.getUserStmt, getUser, userID)
var i User
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.FirstName,
&i.LastName,
&i.Username,
&i.Hash,
&i.Email,
&i.Token,
&i.TokenExpires,
)
return i, err
}
const getUserByToken = `-- name: GetUserByToken :one
SELECT user_id, created_at, updated_at, first_name, last_name, username, hash, email, token, token_expires FROM users
WHERE token = $1 LIMIT 1
`
func (q *Queries) GetUserByToken(ctx context.Context, token uuid.UUID) (User, error) {
row := q.queryRow(ctx, q.getUserByTokenStmt, getUserByToken, token)
var i User
err := row.Scan(
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.FirstName,
&i.LastName,
&i.Username,
&i.Hash,
&i.Email,
&i.Token,
&i.TokenExpires,
)
return i, err
}
const similarEntries = `-- name: SimilarEntries :many
SELECT entry_id, similarity(body, $1) AS sml
FROM entries
WHERE body % $1
ORDER BY sml DESC, body
`
type SimilarEntriesRow struct {
EntryID uuid.UUID `json:"entry_id"`
Sml interface{} `json:"sml"`
}
func (q *Queries) SimilarEntries(ctx context.Context, similarity interface{}) ([]SimilarEntriesRow, error) {
rows, err := q.query(ctx, q.similarEntriesStmt, similarEntries, similarity)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SimilarEntriesRow
for rows.Next() {
var i SimilarEntriesRow
if err := rows.Scan(&i.EntryID, &i.Sml); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const validToken = `-- name: ValidToken :one
SELECT now() < token_created FROM users
WHERE token = $1 LIMIT 1
`
func (q *Queries) ValidToken(ctx context.Context, token uuid.UUID) (bool, error) {
row := q.queryRow(ctx, q.validTokenStmt, validToken, token)
var column_1 bool
err := row.Scan(&column_1)
return column_1, err
}

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module suah.dev/cromp
go 1.13
require (
github.com/google/uuid v1.1.1
github.com/lib/pq v1.3.0
github.com/peterbourgon/ff/v2 v2.0.0
)

20
go.sum Normal file
View File

@ -0,0 +1,20 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff v1.7.0 h1:hknvTgsh90jNBIjPq7xeq32Y9AmSbpXvjrFW4sJwW+A=
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

75
internal/main.go Normal file
View File

@ -0,0 +1,75 @@
package cromp
import (
"bufio"
"os"
"regexp"
"time"
"github.com/google/uuid"
)
// AuthorRE is a regex to grab our Authors
var AuthorRE = regexp.MustCompile(`^author:\s(.*)$`)
// TitleRE matches our article title
var TitleRE = regexp.MustCompile(`^title:\s(.*)$`)
// DateRE matches our article date
var DateRE = regexp.MustCompile(`^date:\s(.*)$`)
// UUIDRE matches a uuid in our file
var UUIDRE = regexp.MustCompile("^id: ([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12})$")
// Header is the header info loaded from a file
type Header struct {
Author string
Title string
Date time.Time
UUID uuid.UUID
}
//ParseFileHeader grabs the header info out of an existing file
func ParseFileHeader(f string) (*Header, error) {
h := &Header{}
file, err := os.Open(f)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(file)
if err != nil {
return nil, err
}
for scanner.Scan() {
var line = scanner.Bytes()
if AuthorRE.Match(line) {
aline := AuthorRE.ReplaceAllString(string(line), "$1")
h.Author = aline
}
if TitleRE.Match(line) {
h.Title = TitleRE.ReplaceAllString(string(line), "$1")
}
if DateRE.Match(line) {
d := DateRE.ReplaceAllString(string(line), "$1")
h.Date, err = time.Parse(time.RFC1123, d)
if err != nil {
return nil, err
}
}
if UUIDRE.Match(line) {
u := UUIDRE.ReplaceAllString(string(line), "$1")
h.UUID, err = uuid.Parse(u)
if err != nil {
return nil, err
}
}
}
if err != nil {
return nil, err
}
return h, nil
}

63
sql/queries/queries.sql Normal file
View File

@ -0,0 +1,63 @@
-- name: CreateEntry :one
INSERT INTO entries (
entry_id, user_id, title, body
) VALUES (
$1, $2, $3, $4
)
RETURNING entry_id, created_at, to_tsvector(body);
-- name: GetEntry :one
SELECT * FROM entries
WHERE entry_id = $1 LIMIT 1;
-- name: GetEntries :many
SELECT * FROM entries
WHERE user_id = $1;
-- name: DeleteEntry :exec
DELETE FROM entries
WHERE entry_id = $1;
-- name: SimilarEntries :many
SELECT entry_id, similarity(body, $1) AS sml
FROM entries
WHERE body % $1
ORDER BY sml DESC, body;
-- name: CreateUser :one
INSERT INTO users (
first_name, last_name, username, email, hash
) VALUES (
$1, $2, $3, $4, hash($5)
)
RETURNING user_id, username, token, token_expires;
-- name: GetUser :one
SELECT * FROM users
WHERE user_id = $1 LIMIT 1;
-- name: GetUserByToken :one
SELECT * FROM users
WHERE token = $1 LIMIT 1;
-- name: AuthUser :one
UPDATE users
SET
token = DEFAULT,
token_expires = DEFAULT
WHERE
username = $2 and
(hash = crypt($1, hash)) = true
RETURNING user_id, created_at, first_name, last_name, username, email, token, token_expires, true as authed;
-- name: DeleteUser :exec
DELETE FROM users CASCADE
WHERE user_id = $1;
-- name: ValidToken :one
SELECT now() < token_created FROM users
WHERE token = $1 LIMIT 1;
-- name: EntriesByToken :many
SELECT * FROM entries
WHERE user_id = (SELECT user_id FROM users WHERE token = $1 limit 1);

33
sql/schema/schema.sql Normal file
View File

@ -0,0 +1,33 @@
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
DROP TABLE IF EXISTS entries;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
user_id BIGSERIAL PRIMARY KEY,
created_at timestamp NOT NULL DEFAULT NOW(),
updated_at timestamp,
first_name text NOT NULL,
last_name text NOT NULL,
username text NOT NULL UNIQUE,
hash text NOT NULL,
email text NOT NULL,
token UUID NOT NULL default gen_random_uuid() UNIQUE,
token_expires timestamp NOT NULL DEFAULT NOW() + INTERVAL '3 days'
);
CREATE TABLE entries (
entry_id UUID NOT NULL default gen_random_uuid() PRIMARY KEY,
user_id int NOT NULL REFERENCES users ON DELETE CASCADE,
created_at timestamp NOT NULL DEFAULT NOW(),
updated_at timestamp,
title text NOT NULL DEFAULT '',
body text NOT NULL DEFAULT ''
);
CREATE INDEX body_idx ON entries USING GIN (body gin_trgm_ops);
CREATE OR REPLACE FUNCTION hash(password text) RETURNS text AS $$
SELECT crypt(password, gen_salt('bf', 10));
$$ LANGUAGE SQL;

9
sqlc.yaml Normal file
View File

@ -0,0 +1,9 @@
version: "1"
packages:
- name: db
emit_json_tags: true
emit_prepared_queries: true
emit_interface: false
path: "db"
queries: "./sql/queries"
schema: "./sql/schema"