initial bits for cromp
This commit is contained in:
commit
ee707eea82
73
cmd/cromp/main.go
Normal file
73
cmd/cromp/main.go
Normal 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
65
cmd/cromps/handlers.go
Normal 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
91
cmd/cromps/main.go
Normal 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
196
db/db.go
Normal 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
32
db/models.go
Normal 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
334
db/queries.sql.go
Normal 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
9
go.mod
Normal 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
20
go.sum
Normal 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
75
internal/main.go
Normal 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
63
sql/queries/queries.sql
Normal 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
33
sql/schema/schema.sql
Normal 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;
|
Loading…
Reference in New Issue
Block a user