-
Notifications
You must be signed in to change notification settings - Fork 107
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d09bd7c
Showing
80 changed files
with
23,290 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:]) | ||
} |
Oops, something went wrong.