Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
benbjohnson committed Dec 10, 2020
0 parents commit d09bd7c
Show file tree
Hide file tree
Showing 80 changed files with 23,290 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.DS_Store
*.test
*.ego.go
/http/assets/css/falcon.css
/http/assets/css/theme.css
/http/assets/css/theme.css.map
/node_modules
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Ben Johnson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
default: generate

# Runs the ego templating generation tool whenever an HTML template changes.
generate: http/html/*.ego
@ego ./http/html

# Removes all ego Go files from the http/html directory.
clean:
@rm http/html/*.ego.go

.PHONY: default generate clean
173 changes: 173 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
WTF Dial
========

This project provides a real-time API for teams to view how f-cked up they
currently are. Each team member provides input to specify the level at which
they feel the team is currently messed up. These values range from 0% (meaning
team feels there are no WTF situations) to 100% (meaning the members feel the
team is completely f-cked).

The idea for this came from [Peter Bourgon's tweets][tweets].

[tweets]: https://twitter.com/peterbourgon/status/765935213507649537


## How to use this repository

This repository was built to help others learn how to build a fully functioning
Go application. It can be used in several ways:

1. As a reference—the code is well documented. Honestly, too documented for most
projects but the goal here is to be as clear as possible for anyone reading
the code.

2. As a walkthrough—companion blog posts will be added that walk through the
various parts of the application and explain the design choices.

You can also see the project structure overview below to get a quick overview
of the application structure.


## Project structure

The `wtf` project organizes code with the following approach:

1. Application domain types go in the root—`User`, `UserService`, `Dial`, etc.
2. Implementations of the application domain go in subpackages—`sqlite`, `http`, etc.
3. Everything is tied together in the `cmd` subpackages—`cmd/wtf` & `cmd/wtfd`.


### Application domain

The application domain is the collection of types which define what your
application does without defining how it does it. For example, if you were to
describe what WTF Dial does to a non-technical person, you would describe it in
terms of _Users_ and _Dials_.

We also include interfaces for managing our application domain data types which
are used as contracts for the underlying implementations. For example, we define
a `wtf.DialService` interface for CRUD (Create/Read/Update/Delete) actions and
SQLite does the actual implementation.

This allows all packages to share a common understanding of what each service
does. We can swap out implementations, or more importantly, we can layer
implementations on top of one another. We could, for example, add a Redis
caching layer on top of our database layer without having the two layers know
about one another as long as they both implement the same common interface.


### Implementation subpackages

Most subpackages are used as an adapter between our application domain and the
technology that we're using to implement the domain. For example,
`sqlite.DialService` implements the `wtf.DialService` using SQLite.

The subpackages generally should not know about one another and should
communicate in terms of the application domain.

These are separated out into the following packages:

- `http`—Implements services over HTTP transport layer.
- `inmem`—Implements in-memory event listener service & subscriptions.
- `sqlite`—Implements services on SQLite storage layer.

There is also a `mock` package which implements simple mocks for each of the
application domain interfaces. This allows each subpackage's unit tests to share
a common set of mocks so layers can be tested in isolation.


### Binary packages

The implementation subpackages are loosely coupled so they need to be wired
together by another package to actually make working software. That's the job
of the `cmd` subpackages which produce the final binary.

There are two binaries:

- `wtfd`—the WTF server
- `wtf`—the client CLI application

Each of these binaries collect the services together in different ways depending
on the use case.

The `wtfd` server binary creates a `sqlite` storage layer and adds the `http`
transport layer on top. The `wtf` client binary doesn't have a storage layer.
It only needs the client side `http` transport layer.

The `cmd` packages are ultimately the interface between the application domain
and the operator. That means that configuration types & CLI flags should live
in these packages.


### Other packages

A few smaller packages don't fall into the organization listed above:

- `csv`—implements a `csv.DialEncoder` for encoding a list of Dial objects to
a writer using the CSV format.
- `http/html`-groups together HTML templates used by the `http` package.



## Development

You can build `wtf` locally by cloning the repository and installing the
[ego](https://github.com/benbjohnson/ego) templating library.

Then run:

```sh
$ make
$ go install ./cmd/...
```

The `wtfd` server uses GitHub for authentication so you'll need to [create a
new GitHub OAuth App](https://github.com/settings/applications/new).

Next, you'll need to setup a configuration file in `~/wtfd.config`:

```toml
[github]
client-id = "00000000000000000000"
client-secret = "0000000000000000000000000000000000000000"

[http]
addr = ":3000"
block-key = "0000000000000000000000000000000000000000000000000000000000000000"
hash-key = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
```

Replace the GitHub `client-id` & `client-secret` with the values from the
GitHub OAuth application you registered.

The `[http]` section can be left as-is for a local environment. The key fields
need random hex values for generating secure cookies but all zeros is ok for
local testing.

Finally, run the `wtfd` server and open the web site at [`http://localhost:3000`](http://localhost:3000):

```
$ $GOPATH/bin/wtfd
```


### SQLite

By default, the SQLite tests run against in-memory databases. However, you can
specify the `-dump` flag for the tests to write data out to temporary files. This
works best when running against a single test.

```sh
$ go test -run=MyTest -dump ./sqlite
DUMP=/tmp/sy9j7nks0zq2vr4s_nswrx8h0000gn/T/375403844/db
```

You can then inspect that database using the `sqlite3` CLI to see its contents.


## Contributing

This application is built for educational purposes so additional functionality
will likely be rejected. Please feel free to submit an issue if you're
interested in seeing something added. Please do not simply submit a pull request.

107 changes: 107 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package wtf

import (
"context"
"fmt"
"time"
)

// Authentication providers.
// Currently we only support GitHub but any OAuth provider could be supported.
const (
AuthSourceGitHub = "github"
)

// Auth represents a set of OAuth credentials. These are linked to a User so a
// single user could authenticate through multiple providers.
//
// The authentication system links users by email address, however, some GitHub
// users don't provide their email publicly so we may not be able to link them
// by email address. It's a moot point, however, as we only support GitHub as
// an OAuth provider.
type Auth struct {
ID int `json:"id"`

// User can have one or more methods of authentication.
// However, only one per source is allowed per user.
UserID int `json:"userID"`
User *User `json:"user"`

// The authentication source & the source provider's user ID.
// Source can only be "github" currently.
Source string `json:"source"`
SourceID string `json:"sourceID"`

// OAuth fields returned from the authentication provider.
// GitHub does not use refresh tokens but the field exists for future providers.
AccessToken string `json:"-"`
RefreshToken string `json:"-"`
Expiry *time.Time `json:"-"`

// Timestamps of creation & last update.
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

// Validate returns an error if any fields are invalid on the Auth object.
// This can be called by the SQLite implementation to do some basic checks.
func (a *Auth) Validate() error {
if a.UserID == 0 {
return Errorf(EINVALID, "User required.")
} else if a.Source == "" {
return Errorf(EINVALID, "Source required.")
} else if a.SourceID == "" {
return Errorf(EINVALID, "Source ID required.")
} else if a.AccessToken == "" {
return Errorf(EINVALID, "Access token required.")
}
return nil
}

// AvatarURL returns a URL to the avatar image hosted by the authentication source.
// Returns an empty string if the authentication source is invalid.
func (a *Auth) AvatarURL(size int) string {
switch a.Source {
case AuthSourceGitHub:
return fmt.Sprintf("https://avatars1.githubusercontent.com/u/%s?s=%d", a.SourceID, size)
default:
return ""
}
}

// AuthService represents a service for managing auths.
type AuthService interface {
// Looks up an authentication object by ID along with the associated user.
// Returns ENOTFOUND if ID does not exist.
FindAuthByID(ctx context.Context, id int) (*Auth, error)

// Retrieves authentication objects based on a filter. Also returns the
// total number of objects that match the filter. This may differ from the
// returned object count if the Limit field is set.
FindAuths(ctx context.Context, filter AuthFilter) ([]*Auth, int, error)

// Creates a new authentication object If a User is attached to auth, then
// the auth object is linked to an existing user. Otherwise a new user
// object is created.
//
// On success, the auth.ID is set to the new authentication ID.
CreateAuth(ctx context.Context, auth *Auth) error

// Permanently deletes an authentication object from the system by ID.
// The parent user object is not removed.
DeleteAuth(ctx context.Context, id int) error
}

// AuthFilter represents a filter accepted by FindAuths().
type AuthFilter struct {
// Filtering fields.
ID *int `json:"id"`
UserID *int `json:"userID"`
Source *string `json:"source"`
SourceID *string `json:"sourceID"`

// Restricts results to a subset of the total range.
// Can be used for pagination.
Offset int `json:"offset"`
Limit int `json:"limit"`
}
55 changes: 55 additions & 0 deletions cmd/wtf/dial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"context"
"flag"
"fmt"
"strings"
)

// DialCommand represents a collection of dial-related subcommands.
type DialCommand struct{}

// Run executes the command which delegates to other subcommands.
func (c *DialCommand) Run(ctx context.Context, args []string) error {
// Shift off the subcommand name, if available.
var cmd string
if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
cmd, args = args[0], args[1:]
}

// Delegete to the appropriate subcommand.
switch cmd {
case "", "list":
return (&DialListCommand{}).Run(ctx, args)
case "create":
return (&DialCreateCommand{}).Run(ctx, args)
case "delete":
return (&DialDeleteCommand{}).Run(ctx, args)
case "members":
return (&DialMembersCommand{}).Run(ctx, args)
case "help":
c.usage()
return flag.ErrHelp
default:
return fmt.Errorf("wtf dial %s: unknown command", cmd)
}
}

// usage prints the subcommand usage to STDOUT.
func (c *DialCommand) usage() {
fmt.Println(`
Manage WTF dials you own or are a member of.
Usage:
wtf dial <command> [arguments]
The commands are:
list list all available dials
create create a new dial
delete remove an existing dial
members view list of members of a dial
`[1:])
}
Loading

0 comments on commit d09bd7c

Please sign in to comment.