You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
5.9 KiB
225 lines
5.9 KiB
// Package handlers provides the http functionality
|
|
package handlers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/contrib/sessions"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/maxibanki/golang-url-shortener/config"
|
|
"github.com/maxibanki/golang-url-shortener/store"
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/google"
|
|
)
|
|
|
|
// Handler holds the funcs and attributes for the
|
|
// http communication
|
|
type Handler struct {
|
|
config config.Handlers
|
|
store store.Store
|
|
engine *gin.Engine
|
|
oAuthConf *oauth2.Config
|
|
}
|
|
|
|
// URLUtil is used to help in- and outgoing requests for json
|
|
// un- and marshalling
|
|
type URLUtil struct {
|
|
URL string
|
|
}
|
|
|
|
type oAuthUser struct {
|
|
Sub string `json:"sub"`
|
|
Name string `json:"name"`
|
|
GivenName string `json:"given_name"`
|
|
FamilyName string `json:"family_name"`
|
|
Profile string `json:"profile"`
|
|
Picture string `json:"picture"`
|
|
Email string `json:"email"`
|
|
EmailVerified bool `json:"email_verified"`
|
|
Gender string `json:"gender"`
|
|
Hd string `json:"hd"`
|
|
}
|
|
|
|
// New initializes the http handlers
|
|
func New(handlerConfig config.Handlers, store store.Store) *Handler {
|
|
h := &Handler{
|
|
config: handlerConfig,
|
|
store: store,
|
|
engine: gin.Default(),
|
|
}
|
|
h.setHandlers()
|
|
h.initOAuth()
|
|
return h
|
|
}
|
|
|
|
func (h *Handler) setHandlers() {
|
|
if !h.config.EnableGinDebugMode {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
h.engine.POST("/api/v1/create", h.handleCreate)
|
|
h.engine.POST("/api/v1/info", h.handleInfo)
|
|
// h.engine.Static("/static", "static/src")
|
|
h.engine.NoRoute(h.handleAccess)
|
|
}
|
|
|
|
func (h *Handler) initOAuth() {
|
|
store := sessions.NewCookieStore([]byte("secret"))
|
|
|
|
h.oAuthConf = &oauth2.Config{
|
|
ClientID: h.config.OAuth.Google.ClientID,
|
|
ClientSecret: h.config.OAuth.Google.ClientSecret,
|
|
RedirectURL: "http://127.0.0.1:3000/api/v1/auth/",
|
|
Scopes: []string{
|
|
"https://www.googleapis.com/auth/userinfo.email",
|
|
},
|
|
Endpoint: google.Endpoint,
|
|
}
|
|
h.engine.Use(sessions.Sessions("goquestsession", store))
|
|
h.engine.GET("/api/v1/login", h.handleGoogleLogin)
|
|
|
|
private := h.engine.Group("/api/v1/auth")
|
|
private.Use(h.handleGoogleAuth)
|
|
private.GET("/", h.handleGoogleCallback)
|
|
private.GET("/api", func(c *gin.Context) {
|
|
c.JSON(200, gin.H{"message": "Hello from private for groups"})
|
|
})
|
|
}
|
|
|
|
func (h *Handler) randToken() string {
|
|
b := make([]byte, 32)
|
|
rand.Read(b)
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
}
|
|
|
|
func (h *Handler) handleGoogleAuth(c *gin.Context) {
|
|
// Handle the exchange code to initiate a transport.
|
|
session := sessions.Default(c)
|
|
retrievedState := session.Get("state")
|
|
if retrievedState != c.Query("state") {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Errorf("Invalid session state: %s", retrievedState)})
|
|
return
|
|
}
|
|
|
|
token, err := h.oAuthConf.Exchange(oauth2.NoContext, c.Query("code"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
client := h.oAuthConf.Client(oauth2.NoContext, token)
|
|
userinfo, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
defer userinfo.Body.Close()
|
|
data, err := ioutil.ReadAll(userinfo.Body)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Could not read body: %v", err)})
|
|
return
|
|
}
|
|
|
|
var user oAuthUser
|
|
err = json.Unmarshal(data, &user)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Decoding userinfo failed: %v", err)})
|
|
return
|
|
}
|
|
c.Set("user", user)
|
|
}
|
|
|
|
func (h *Handler) handleGoogleLogin(c *gin.Context) {
|
|
state := h.randToken()
|
|
session := sessions.Default(c)
|
|
session.Set("state", state)
|
|
session.Save()
|
|
c.Redirect(http.StatusTemporaryRedirect, h.oAuthConf.AuthCodeURL(state))
|
|
}
|
|
|
|
func (h *Handler) handleGoogleCallback(ctx *gin.Context) {
|
|
ctx.JSON(http.StatusOK, gin.H{"Hello": "from private", "user": ctx.MustGet("user").(oAuthUser)})
|
|
}
|
|
|
|
// handleCreate handles requests to create an entry
|
|
func (h *Handler) handleCreate(c *gin.Context) {
|
|
var data struct {
|
|
URL string
|
|
}
|
|
err := c.ShouldBind(&data)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
id, err := h.store.CreateEntry(data.URL, c.ClientIP())
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
data.URL = h.getSchemaAndHost(c) + "/" + id
|
|
c.JSON(http.StatusOK, data)
|
|
}
|
|
|
|
func (h *Handler) getSchemaAndHost(c *gin.Context) string {
|
|
protocol := "http"
|
|
if c.Request.TLS != nil {
|
|
protocol = "https"
|
|
}
|
|
return fmt.Sprintf("%s://%s", protocol, c.Request.Host)
|
|
}
|
|
|
|
// handleInfo is the http handler for getting the infos
|
|
func (h *Handler) handleInfo(c *gin.Context) {
|
|
var data struct {
|
|
ID string `binding:"required"`
|
|
}
|
|
err := c.ShouldBind(&data)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
entry, err := h.store.GetEntryByID(data.ID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
entry.RemoteAddr = ""
|
|
c.JSON(http.StatusOK, entry)
|
|
}
|
|
|
|
// handleAccess handles the access for incoming requests
|
|
func (h *Handler) handleAccess(c *gin.Context) {
|
|
var id string
|
|
if len(c.Request.URL.Path) > 1 {
|
|
id = c.Request.URL.Path[1:]
|
|
}
|
|
entry, err := h.store.GetEntryByID(id)
|
|
if err == store.ErrIDIsEmpty || err == store.ErrNoEntryFound {
|
|
return // return normal 404 error if such an error occurs
|
|
} else if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
err = h.store.IncreaseVisitCounter(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Redirect(http.StatusTemporaryRedirect, entry.URL)
|
|
}
|
|
|
|
// Listen starts the http server
|
|
func (h *Handler) Listen() error {
|
|
return h.engine.Run(h.config.ListenAddr)
|
|
}
|
|
|
|
// CloseStore stops the http server and the closes the db gracefully
|
|
func (h *Handler) CloseStore() error {
|
|
return h.store.Close()
|
|
}
|
|
|