diff options
| author | r <r@freesoftwareextremist.com> | 2019-12-13 18:08:26 +0000 | 
|---|---|---|
| committer | r <r@freesoftwareextremist.com> | 2019-12-13 18:26:24 +0000 | 
| commit | 5e4da01c3ae3ae2e870faba9085d9d9213c01c29 (patch) | |
| tree | 39d6f1e76b901549f194ddbac3c6cb82e0abd019 /service | |
| download | bloat-5e4da01c3ae3ae2e870faba9085d9d9213c01c29.tar.gz bloat-5e4da01c3ae3ae2e870faba9085d9d9213c01c29.zip | |
Initial commit
Diffstat (limited to 'service')
| -rw-r--r-- | service/auth.go | 151 | ||||
| -rw-r--r-- | service/logging.go | 117 | ||||
| -rw-r--r-- | service/service.go | 285 | ||||
| -rw-r--r-- | service/transport.go | 165 | 
4 files changed, 718 insertions, 0 deletions
| diff --git a/service/auth.go b/service/auth.go new file mode 100644 index 0000000..cb442a7 --- /dev/null +++ b/service/auth.go @@ -0,0 +1,151 @@ +package service + +import ( +	"context" +	"errors" +	"io" +	"mastodon" +	"web/model" +) + +var ( +	ErrInvalidSession = errors.New("invalid session") +) + +type authService struct { +	sessionRepo model.SessionRepository +	appRepo     model.AppRepository +	Service +} + +func NewAuthService(sessionRepo model.SessionRepository, appRepo model.AppRepository, s Service) Service { +	return &authService{sessionRepo, appRepo, s} +} + +func getSessionID(ctx context.Context) (sessionID string, err error) { +	sessionID, ok := ctx.Value("session_id").(string) +	if !ok || len(sessionID) < 1 { +		return "", ErrInvalidSession +	} +	return sessionID, nil +} + +func (s *authService) getClient(ctx context.Context) (c *mastodon.Client, err error) { +	sessionID, err := getSessionID(ctx) +	if err != nil { +		return nil, ErrInvalidSession +	} +	session, err := s.sessionRepo.Get(sessionID) +	if err != nil { +		return nil, ErrInvalidSession +	} +	client, err := s.appRepo.Get(session.InstanceURL) +	if err != nil { +		return +	} +	c = mastodon.NewClient(&mastodon.Config{ +		Server:       session.InstanceURL, +		ClientID:     client.ClientID, +		ClientSecret: client.ClientSecret, +		AccessToken:  session.AccessToken, +	}) +	return c, nil +} + +func (s *authService) GetAuthUrl(ctx context.Context, instance string) ( +	redirectUrl string, sessionID string, err error) { +	return s.Service.GetAuthUrl(ctx, instance) +} + +func (s *authService) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client, +	code string) (token string, err error) { +	sessionID, err = getSessionID(ctx) +	if err != nil { +		return +	} +	c, err = s.getClient(ctx) +	if err != nil { +		return +	} + +	token, err = s.Service.GetUserToken(ctx, sessionID, c, code) +	if err != nil { +		return +	} + +	err = s.sessionRepo.Update(sessionID, token) +	if err != nil { +		return +	} + +	return +} + +func (s *authService) ServeHomePage(ctx context.Context, client io.Writer) (err error) { +	return s.Service.ServeHomePage(ctx, client) +} + +func (s *authService) ServeErrorPage(ctx context.Context, client io.Writer, err error) { +	s.Service.ServeErrorPage(ctx, client, err) +} + +func (s *authService) ServeSigninPage(ctx context.Context, client io.Writer) (err error) { +	return s.Service.ServeSigninPage(ctx, client) +} + +func (s *authService) ServeTimelinePage(ctx context.Context, client io.Writer, +	c *mastodon.Client, maxID string, sinceID string, minID string) (err error) { +	c, err = s.getClient(ctx) +	if err != nil { +		return +	} +	return s.Service.ServeTimelinePage(ctx, client, c, maxID, sinceID, minID) +} + +func (s *authService) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) { +	c, err = s.getClient(ctx) +	if err != nil { +		return +	} +	return s.Service.ServeThreadPage(ctx, client, c, id, reply) +} + +func (s *authService) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	c, err = s.getClient(ctx) +	if err != nil { +		return +	} +	return s.Service.Like(ctx, client, c, id) +} + +func (s *authService) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	c, err = s.getClient(ctx) +	if err != nil { +		return +	} +	return s.Service.UnLike(ctx, client, c, id) +} + +func (s *authService) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	c, err = s.getClient(ctx) +	if err != nil { +		return +	} +	return s.Service.Retweet(ctx, client, c, id) +} + +func (s *authService) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	c, err = s.getClient(ctx) +	if err != nil { +		return +	} +	return s.Service.UnRetweet(ctx, client, c, id) +} + +func (s *authService) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error) { +	c, err = s.getClient(ctx) +	if err != nil { +		return +	} +	return s.Service.PostTweet(ctx, client, c, content, replyToID) +} diff --git a/service/logging.go b/service/logging.go new file mode 100644 index 0000000..b11599e --- /dev/null +++ b/service/logging.go @@ -0,0 +1,117 @@ +package service + +import ( +	"context" +	"io" +	"log" +	"mastodon" +	"time" +) + +type loggingService struct { +	logger *log.Logger +	Service +} + +func NewLoggingService(logger *log.Logger, s Service) Service { +	return &loggingService{logger, s} +} + +func (s *loggingService) GetAuthUrl(ctx context.Context, instance string) ( +	redirectUrl string, sessionID string, err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, instance=%v, took=%v, err=%v\n", +			"GetAuthUrl", instance, time.Since(begin), err) +	}(time.Now()) +	return s.Service.GetAuthUrl(ctx, instance) +} + +func (s *loggingService) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client, +	code string) (token string, err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, session_id=%v, code=%v, took=%v, err=%v\n", +			"GetUserToken", sessionID, code, time.Since(begin), err) +	}(time.Now()) +	return s.Service.GetUserToken(ctx, sessionID, c, code) +} + +func (s *loggingService) ServeHomePage(ctx context.Context, client io.Writer) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, took=%v, err=%v\n", +			"ServeHomePage", time.Since(begin), err) +	}(time.Now()) +	return s.Service.ServeHomePage(ctx, client) +} + +func (s *loggingService) ServeErrorPage(ctx context.Context, client io.Writer, err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, err=%v, took=%v\n", +			"ServeErrorPage", err, time.Since(begin)) +	}(time.Now()) +	s.Service.ServeErrorPage(ctx, client, err) +} + +func (s *loggingService) ServeSigninPage(ctx context.Context, client io.Writer) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, took=%v, err=%v\n", +			"ServeSigninPage", time.Since(begin), err) +	}(time.Now()) +	return s.Service.ServeSigninPage(ctx, client) +} + +func (s *loggingService) ServeTimelinePage(ctx context.Context, client io.Writer, +	c *mastodon.Client, maxID string, sinceID string, minID string) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, max_id=%v, since_id=%v, min_id=%v, took=%v, err=%v\n", +			"ServeTimelinePage", maxID, sinceID, minID, time.Since(begin), err) +	}(time.Now()) +	return s.Service.ServeTimelinePage(ctx, client, c, maxID, sinceID, minID) +} + +func (s *loggingService) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, id=%v, reply=%v, took=%v, err=%v\n", +			"ServeThreadPage", id, reply, time.Since(begin), err) +	}(time.Now()) +	return s.Service.ServeThreadPage(ctx, client, c, id, reply) +} + +func (s *loggingService) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", +			"Like", id, time.Since(begin), err) +	}(time.Now()) +	return s.Service.Like(ctx, client, c, id) +} + +func (s *loggingService) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", +			"UnLike", id, time.Since(begin), err) +	}(time.Now()) +	return s.Service.UnLike(ctx, client, c, id) +} + +func (s *loggingService) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", +			"Retweet", id, time.Since(begin), err) +	}(time.Now()) +	return s.Service.Retweet(ctx, client, c, id) +} + +func (s *loggingService) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", +			"UnRetweet", id, time.Since(begin), err) +	}(time.Now()) +	return s.Service.UnRetweet(ctx, client, c, id) +} + +func (s *loggingService) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error) { +	defer func(begin time.Time) { +		s.logger.Printf("method=%v, content=%v, reply_to_id=%v, took=%v, err=%v\n", +			"PostTweet", content, replyToID, time.Since(begin), err) +	}(time.Now()) +	return s.Service.PostTweet(ctx, client, c, content, replyToID) +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..7088a9b --- /dev/null +++ b/service/service.go @@ -0,0 +1,285 @@ +package service + +import ( +	"bytes" +	"context" +	"encoding/json" +	"errors" +	"fmt" +	"io" +	"net/http" +	"net/url" +	"path" +	"strings" + +	"mastodon" +	"web/model" +	"web/renderer" +	"web/util" +) + +var ( +	ErrInvalidArgument = errors.New("invalid argument") +	ErrInvalidToken    = errors.New("invalid token") +	ErrInvalidClient   = errors.New("invalid client") +) + +type Service interface { +	ServeHomePage(ctx context.Context, client io.Writer) (err error) +	GetAuthUrl(ctx context.Context, instance string) (url string, sessionID string, err error) +	GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client, token string) (accessToken string, err error) +	ServeErrorPage(ctx context.Context, client io.Writer, err error) +	ServeSigninPage(ctx context.Context, client io.Writer) (err error) +	ServeTimelinePage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, sinceID string, minID string) (err error) +	ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) +	Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) +	UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) +	Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) +	UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) +	PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error) +} + +type service struct { +	clientName    string +	clientScope   string +	clientWebsite string +	renderer      renderer.Renderer +	sessionRepo   model.SessionRepository +	appRepo       model.AppRepository +} + +func NewService(clientName string, clientScope string, clientWebsite string, +	renderer renderer.Renderer, sessionRepo model.SessionRepository, +	appRepo model.AppRepository) Service { +	return &service{ +		clientName:    clientName, +		clientScope:   clientScope, +		clientWebsite: clientWebsite, +		renderer:      renderer, +		sessionRepo:   sessionRepo, +		appRepo:       appRepo, +	} +} + +func (svc *service) GetAuthUrl(ctx context.Context, instance string) ( +	redirectUrl string, sessionID string, err error) { +	if !strings.HasPrefix(instance, "https://") { +		instance = "https://" + instance +	} + +	sessionID = util.NewSessionId() +	err = svc.sessionRepo.Add(model.Session{ +		ID:          sessionID, +		InstanceURL: instance, +	}) +	if err != nil { +		return +	} + +	app, err := svc.appRepo.Get(instance) +	if err != nil { +		if err != model.ErrAppNotFound { +			return +		} + +		var mastoApp *mastodon.Application +		mastoApp, err = mastodon.RegisterApp(ctx, &mastodon.AppConfig{ +			Server:       instance, +			ClientName:   svc.clientName, +			Scopes:       svc.clientScope, +			Website:      svc.clientWebsite, +			RedirectURIs: svc.clientWebsite + "/oauth_callback", +		}) +		if err != nil { +			return +		} + +		app = model.App{ +			InstanceURL:  instance, +			ClientID:     mastoApp.ClientID, +			ClientSecret: mastoApp.ClientSecret, +		} + +		err = svc.appRepo.Add(app) +		if err != nil { +			return +		} +	} + +	u, err := url.Parse(path.Join(instance, "/oauth/authorize")) +	if err != nil { +		return +	} + +	q := make(url.Values) +	q.Set("scope", "read write follow") +	q.Set("client_id", app.ClientID) +	q.Set("response_type", "code") +	q.Set("redirect_uri", svc.clientWebsite+"/oauth_callback") +	u.RawQuery = q.Encode() + +	redirectUrl = u.String() + +	return +} + +func (svc *service) GetUserToken(ctx context.Context, sessionID string, c *mastodon.Client, +	code string) (token string, err error) { +	if len(code) < 1 { +		err = ErrInvalidArgument +		return +	} + +	session, err := svc.sessionRepo.Get(sessionID) +	if err != nil { +		return +	} + +	app, err := svc.appRepo.Get(session.InstanceURL) +	if err != nil { +		return +	} + +	data := &bytes.Buffer{} +	err = json.NewEncoder(data).Encode(map[string]string{ +		"client_id":     app.ClientID, +		"client_secret": app.ClientSecret, +		"grant_type":    "authorization_code", +		"code":          code, +		"redirect_uri":  svc.clientWebsite + "/oauth_callback", +	}) +	if err != nil { +		return +	} + +	resp, err := http.Post(app.InstanceURL+"/oauth/token", "application/json", data) +	if err != nil { +		return +	} +	defer resp.Body.Close() + +	var res struct { +		AccessToken string `json:"access_token"` +	} + +	err = json.NewDecoder(resp.Body).Decode(&res) +	if err != nil { +		return +	} +	/* +		err = c.AuthenticateToken(ctx, code, svc.clientWebsite+"/oauth_callback") +		if err != nil { +			return +		} +		err = svc.sessionRepo.Update(sessionID, c.GetAccessToken(ctx)) +	*/ + +	return res.AccessToken, nil +} + +func (svc *service) ServeHomePage(ctx context.Context, client io.Writer) (err error) { +	err = svc.renderer.RenderHomePage(ctx, client) +	if err != nil { +		return +	} + +	return +} + +func (svc *service) ServeErrorPage(ctx context.Context, client io.Writer, err error) { +	svc.renderer.RenderErrorPage(ctx, client, err) +} + +func (svc *service) ServeSigninPage(ctx context.Context, client io.Writer) (err error) { +	err = svc.renderer.RenderSigninPage(ctx, client) +	if err != nil { +		return +	} + +	return +} + +func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer, +	c *mastodon.Client, maxID string, sinceID string, minID string) (err error) { + +	var hasNext, hasPrev bool +	var nextLink, prevLink string + +	var pg = mastodon.Pagination{ +		MaxID:   maxID, +		SinceID: sinceID, +		MinID:   minID, +		Limit:   20, +	} + +	statuses, err := c.GetTimelineHome(ctx, &pg) +	if err != nil { +		return err +	} + +	if len(pg.MaxID) > 0 { +		hasNext = true +		nextLink = fmt.Sprintf("/timeline?max_id=%s", pg.MaxID) +	} +	if len(pg.SinceID) > 0 { +		hasPrev = true +		prevLink = fmt.Sprintf("/timeline?since_id=%s", pg.SinceID) +	} + +	data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink) +	err = svc.renderer.RenderTimelinePage(ctx, client, data) +	if err != nil { +		return +	} + +	return +} + +func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) { +	status, err := c.GetStatus(ctx, id) +	if err != nil { +		return +	} + +	context, err := c.GetStatusContext(ctx, id) +	if err != nil { +		return +	} + +	data := renderer.NewThreadPageTemplateData(status, context, reply, id) +	err = svc.renderer.RenderThreadPage(ctx, client, data) +	if err != nil { +		return +	} + +	return +} + +func (svc *service) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	_, err = c.Favourite(ctx, id) +	return +} + +func (svc *service) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	_, err = c.Unfavourite(ctx, id) +	return +} + +func (svc *service) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	_, err = c.Reblog(ctx, id) +	return +} + +func (svc *service) UnRetweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { +	_, err = c.Unreblog(ctx, id) +	return +} + +func (svc *service) PostTweet(ctx context.Context, client io.Writer, c *mastodon.Client, content string, replyToID string) (err error) { +	tweet := &mastodon.Toot{ +		Status:      content, +		InReplyToID: replyToID, +	} +	_, err = c.PostStatus(ctx, tweet) +	return +} diff --git a/service/transport.go b/service/transport.go new file mode 100644 index 0000000..f4f5ed7 --- /dev/null +++ b/service/transport.go @@ -0,0 +1,165 @@ +package service + +import ( +	"context" +	"fmt" +	"net/http" +	"path" + +	"github.com/gorilla/mux" +) + +var ( +	ctx       = context.Background() +	cookieAge = "31536000" +) + +func getContextWithSession(ctx context.Context, req *http.Request) context.Context { +	sessionID, err := req.Cookie("session_id") +	if err != nil { +		return ctx +	} +	return context.WithValue(ctx, "session_id", sessionID.Value) +} + +func NewHandler(s Service, staticDir string) http.Handler { +	r := mux.NewRouter() + +	r.PathPrefix("/static").Handler(http.StripPrefix("/static", +		http.FileServer(http.Dir(path.Join(".", staticDir))))) + +	r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { +		err := s.ServeHomePage(ctx, w) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} +	}).Methods(http.MethodGet) + +	r.HandleFunc("/signin", func(w http.ResponseWriter, req *http.Request) { +		err := s.ServeSigninPage(ctx, w) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} +	}).Methods(http.MethodGet) + +	r.HandleFunc("/signin", func(w http.ResponseWriter, req *http.Request) { +		instance := req.FormValue("instance") +		url, sessionId, err := s.GetAuthUrl(ctx, instance) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} + +		w.Header().Add("Set-Cookie", fmt.Sprintf("session_id=%s;max-age=%s", sessionId, cookieAge)) +		w.Header().Add("Location", url) +		w.WriteHeader(http.StatusSeeOther) +	}).Methods(http.MethodPost) + +	r.HandleFunc("/oauth_callback", func(w http.ResponseWriter, req *http.Request) { +		ctx := getContextWithSession(context.Background(), req) +		token := req.URL.Query().Get("code") +		_, err := s.GetUserToken(ctx, "", nil, token) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} + +		w.Header().Add("Location", "/timeline") +		w.WriteHeader(http.StatusSeeOther) +	}).Methods(http.MethodGet) + +	r.HandleFunc("/timeline", func(w http.ResponseWriter, req *http.Request) { +		ctx := getContextWithSession(context.Background(), req) + +		maxID := req.URL.Query().Get("max_id") +		sinceID := req.URL.Query().Get("since_id") +		minID := req.URL.Query().Get("min_id") + +		err := s.ServeTimelinePage(ctx, w, nil, maxID, sinceID, minID) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} +	}).Methods(http.MethodGet) + +	r.HandleFunc("/thread/{id}", func(w http.ResponseWriter, req *http.Request) { +		ctx := getContextWithSession(context.Background(), req) +		id, _ := mux.Vars(req)["id"] +		reply := req.URL.Query().Get("reply") +		err := s.ServeThreadPage(ctx, w, nil, id, len(reply) > 1) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} +	}).Methods(http.MethodGet) + +	r.HandleFunc("/like/{id}", func(w http.ResponseWriter, req *http.Request) { +		ctx := getContextWithSession(context.Background(), req) +		id, _ := mux.Vars(req)["id"] +		err := s.Like(ctx, w, nil, id) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} + +		w.Header().Add("Location", req.Header.Get("Referer")) +		w.WriteHeader(http.StatusSeeOther) +	}).Methods(http.MethodGet) + +	r.HandleFunc("/unlike/{id}", func(w http.ResponseWriter, req *http.Request) { +		ctx := getContextWithSession(context.Background(), req) +		id, _ := mux.Vars(req)["id"] +		err := s.UnLike(ctx, w, nil, id) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} + +		w.Header().Add("Location", req.Header.Get("Referer")) +		w.WriteHeader(http.StatusSeeOther) +	}).Methods(http.MethodGet) + +	r.HandleFunc("/retweet/{id}", func(w http.ResponseWriter, req *http.Request) { +		ctx := getContextWithSession(context.Background(), req) +		id, _ := mux.Vars(req)["id"] +		err := s.Retweet(ctx, w, nil, id) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} + +		w.Header().Add("Location", req.Header.Get("Referer")) +		w.WriteHeader(http.StatusSeeOther) +	}).Methods(http.MethodGet) + +	r.HandleFunc("/unretweet/{id}", func(w http.ResponseWriter, req *http.Request) { +		ctx := getContextWithSession(context.Background(), req) +		id, _ := mux.Vars(req)["id"] +		err := s.UnRetweet(ctx, w, nil, id) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} + +		w.Header().Add("Location", req.Header.Get("Referer")) +		w.WriteHeader(http.StatusSeeOther) +	}).Methods(http.MethodGet) + +	r.HandleFunc("/post", func(w http.ResponseWriter, req *http.Request) { +		ctx := getContextWithSession(context.Background(), req) +		content := req.FormValue("content") +		replyToID := req.FormValue("reply_to_id") +		err := s.PostTweet(ctx, w, nil, content, replyToID) +		if err != nil { +			s.ServeErrorPage(ctx, w, err) +			return +		} + +		w.Header().Add("Location", req.Header.Get("Referer")) +		w.WriteHeader(http.StatusSeeOther) +	}).Methods(http.MethodPost) + +	return r +} | 
