Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
JWT_SIGNING_KEY=testing
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
Expand Down
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ $(MAKEFILE):

-include $(MAKEFILE)

# set enviroment variables from .env file
include .env
export $(shell sed 's/=.*//' .env)

dependencies-frontend: dependencies
$(YARN) install

Expand All @@ -35,3 +39,6 @@ build: dependencies-frontend
## Compiles the dashboard assets, and serve the dashboard through its API
serve: build
go run server/cmd/code-annotation/*

gorun:
go run server/cmd/code-annotation/*
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ In order to evaluate quality of ML models, as well as to create “ImageNet for

## Installation

### Github OAuth tokens

First you need OAuth application on github. [Read how to create it](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/).

On a [page](https://github.com/settings/developers) with your application you will need `Client ID` and `Client Secret`

Copy `.env.tpl` to `.env` and set tokens there.

### Docker

```bash
docker build -t srcd/code-annotation .
docker run --rm -p 8080:8080 srcd/code-annotation
docker run --env-file .env --rm -p 8080:8080 srcd/code-annotation
```

### Non-docker

```bash
go get <here will be path>
cd $GOPATH/<here will be path>
go get github.com/src-d/code-annotation/...
cd $GOPATH/github.com/src-d/code-annotation
make serve
```

Expand Down
55 changes: 54 additions & 1 deletion server/cmd/code-annotation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,68 @@ package main

import (
"net/http"
"os"

"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/kelseyhightower/envconfig"
"github.com/sirupsen/logrus"

"github.com/src-d/code-annotation/server/handler"
"github.com/src-d/code-annotation/server/repository"
"github.com/src-d/code-annotation/server/service"
)

func main() {
// create repos
userRepo := &repository.Users{}

// create services
var oauthConfig service.OAuthConfig
envconfig.MustProcess("oauth", &oauthConfig)
oauth := service.NewOAuth(oauthConfig.ClientID, oauthConfig.ClientSecret)

var jwtConfig service.JWTConfig
envconfig.MustProcess("jwt", &jwtConfig)
jwt := service.NewJWT(jwtConfig.SigningKey)

// routing
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Mount("/", http.FileServer(http.Dir("build")))
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
headers.Set("Access-Control-Allow-Origin", "*")
headers.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
headers.Set("Access-Control-Allow-Headers", "Location, Authorization")
if r.Method == "OPTIONS" {
return
}
h.ServeHTTP(w, r)
})
})

r.Get("/login", handler.Login(oauth))
r.Get("/oauth-callback", handler.OAuthCallback(oauth, jwt, userRepo))

// protected handlers
r.Route("/api", func(r chi.Router) {
r.Use(jwt.Middleware)

r.Get("/me", handler.Me(userRepo))
})

// frontend
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
filepath := "build" + r.URL.Path
if _, err := os.Stat(filepath); err == nil {
http.ServeFile(w, r, filepath)
return
}
http.ServeFile(w, r, "build/index.html")
})

logrus.Info("running...")
http.ListenAndServe(":8080", r)
}
52 changes: 52 additions & 0 deletions server/handler/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package handler

import (
"net/http"

"github.com/sirupsen/logrus"
"github.com/src-d/code-annotation/server/repository"
"github.com/src-d/code-annotation/server/service"
)

// Login handler redirects user to oauth provider
func Login(oAuth *service.OAuth) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
url := oAuth.MakeAuthURL()
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
}

// OAuthCallback makes exchange with oauth provider, gets&creates user and redirects to index page with JWT token
func OAuthCallback(oAuth *service.OAuth, jwt *service.JWT, userRepo *repository.Users) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := oAuth.ValidateState(r.FormValue("state")); err != nil {
writeResponse(w, respErr(http.StatusBadRequest, err.Error()))
return
}

code := r.FormValue("code")
user, err := oAuth.GetUser(r.Context(), code)
if err != nil {
logrus.Errorf("oauth get user error: %s", err)
// FIXME can it be not server error? for wrong code
writeResponse(w, respInternalErr())
return
}

// FIXME with real repo we need to check does user exists already or not
if err := userRepo.Create(user); err != nil {
logrus.Errorf("can't create user: %s", err)
writeResponse(w, respInternalErr())
return
}

token, err := jwt.MakeToken(user)
if err != nil {
logrus.Errorf("make jwt token error: %s", err)
writeResponse(w, respInternalErr())
return
}
url := "/?token=" + token
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
}
50 changes: 50 additions & 0 deletions server/handler/render.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package handler

import (
"encoding/json"
"net/http"
)

func respOK(d interface{}) response {
return response{
statusCode: http.StatusOK,
Data: d,
}
}

func respErr(statusCode int, msg string) response {
return response{
statusCode: statusCode,
Errors: []errObj{errObj{Title: msg}},
}
}

func respInternalErr() response {
return respErr(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}

type errObj struct {
Title string `json:"title"`
}

type response struct {
statusCode int
Data interface{} `json:"data,omitempty"`
Errors []errObj `json:"errors,omitempty"`
}

type renderFunc func(r *http.Request) response

func render(fn renderFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
writeResponse(w, fn(r))
}
}

func writeResponse(w http.ResponseWriter, resp response) {
w.WriteHeader(resp.statusCode)
if err := json.NewEncoder(w).Encode(&resp); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
}
23 changes: 23 additions & 0 deletions server/handler/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package handler

import (
"net/http"

"github.com/src-d/code-annotation/server/repository"
"github.com/src-d/code-annotation/server/service"
)

// Me handler returns information about current user
func Me(usersRepo *repository.Users) http.HandlerFunc {
return render(func(r *http.Request) response {
uID := service.GetUserID(r.Context())
if uID == 0 {
return respErr(http.StatusInternalServerError, "no user id in context")
}
u, err := usersRepo.Get(uID)
if err != nil {
return respErr(http.StatusNotFound, "user not found")
}
return respOK(u)
})
}
8 changes: 8 additions & 0 deletions server/model/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package model

type User struct {
ID int `json:"id"`
Login string `json:"login"`
Username string `json:"username"`
AvatarURL string `json:"avatarURL"`
}
30 changes: 30 additions & 0 deletions server/repository/users.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package repository

import (
"fmt"

"github.com/src-d/code-annotation/server/model"
)

type Users struct {
users []*model.User
}

func (r *Users) Create(m *model.User) error {
for _, u := range r.users {
if u.ID == m.ID {
return nil
}
}
r.users = append(r.users, m)
return nil
}

func (r *Users) Get(id int) (*model.User, error) {
for _, u := range r.users {
if u.ID == id {
return u, nil
}
}
return nil, fmt.Errorf("user with id %d not found", id)
}
84 changes: 84 additions & 0 deletions server/service/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package service

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/src-d/code-annotation/server/model"

jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
)

// Strips 'Bearer ' prefix from bearer token string
func stripBearerPrefixFromTokenString(tok string) (string, error) {
// Should be a bearer token
if len(tok) > 6 && strings.ToUpper(tok[0:7]) == "BEARER " {
return tok[7:], nil
}
return tok, nil
}

var extractor = &request.PostExtractionFilter{
Extractor: request.HeaderExtractor{"Authorization"},
Filter: stripBearerPrefixFromTokenString,
}

// JWTConfig defines enviroment variables for JWT
type JWTConfig struct {
SigningKey string `envconfig:"SIGNING_KEY" required:"true"`
}

// JWT service abstracts JWT implementation
type JWT struct {
signingKey []byte
}

// NewJWT return new JWT service
func NewJWT(signingKey string) *JWT {
return &JWT{signingKey: []byte(signingKey)}
}

type userIDContext int

const userIDKey userIDContext = 1

type jwtClaim struct {
ID int
jwt.StandardClaims
}

// MakeToken generates token string for a user
func (j *JWT) MakeToken(user *model.User) (string, error) {
claims := &jwtClaim{ID: user.ID}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := t.SignedString(j.signingKey)
if err != nil {
return "", fmt.Errorf("can't sign jwt token: %s", err)
}
return ss, nil
}

// Middleware return http.Handler which validates token and set user id in context
func (j *JWT) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var claims jwtClaim
_, err := request.ParseFromRequestWithClaims(r, extractor, &claims, func(token *jwt.Token) (interface{}, error) {
return j.signingKey, nil
})
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
r = r.WithContext(context.WithValue(r.Context(), userIDKey, claims.ID))
next.ServeHTTP(w, r)
})
}

// GetUserID gets user id setted by JWT middleware
func GetUserID(ctx context.Context) int {
id, _ := ctx.Value(userIDKey).(int)
return id
}
Loading