commit 1b118841211da90381dd950cafa13ead78b7f589
parent b6087cc08d0228f2304bbc3c6b97b86aa8d3c3f1
Author: tsmethurst <tobi.smethurst@klarrio.com>
Date: Thu, 18 Mar 2021 23:27:43 +0100
auth flow working for code
Diffstat:
14 files changed, 274 insertions(+), 128 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -3,3 +3,6 @@
# exclude built documentation, since readthedocs will build it for us anyway
/docs/_build
+
+# exclude coverage report
+cp.out
diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go
@@ -95,6 +95,14 @@ func main() {
Value: "postgres",
EnvVars: []string{envNames.DbDatabase},
},
+
+ // TEMPLATE FLAGS
+ &cli.StringFlag{
+ Name: flagNames.TemplateBaseDir,
+ Usage: "Basedir for html templating files for rendering pages and composing emails",
+ Value: "./web/template/",
+ EnvVars: []string{envNames.TemplateBaseDir},
+ },
},
Commands: []*cli.Command{
{
@@ -137,12 +145,12 @@ func main() {
func runAction(c *cli.Context, a action.GTSAction) error {
// create a new *config.Config based on the config path provided...
- conf, err := config.New(c.String(config.GetFlagNames().ConfigPath))
+ conf, err := config.FromFile(c.String(config.GetFlagNames().ConfigPath))
if err != nil {
return fmt.Errorf("error creating config: %s", err)
}
// ... and the flags set on the *cli.Context by urfave
- conf.ParseFlags(c)
+ conf.ParseCLIFlags(c)
// create a logger with the log level, formatting, and output splitter already set
log, err := log.New(conf.LogLevel)
diff --git a/example/config.yaml b/example/config.yaml
@@ -60,3 +60,10 @@ db:
# Examples: ["mydb","postgres","gotosocial"]
# Default: "postgres"
database: "postgres"
+
+# Config pertaining to templating of web pages/email notifications and the like
+template:
+ # String. Directory from which gotosocial will attempt to load html templates (.tmpl files).
+ # Examples: ["/some/absolute/path/", "./relative/path/", "../../some/weird/path/"]
+ # Default: "./web/template/"
+ baseDir: "./web/template/"
diff --git a/go.mod b/go.mod
@@ -11,7 +11,7 @@ require (
github.com/go-session/session v3.1.2+incompatible
github.com/golang/mock v1.4.4 // indirect
github.com/google/uuid v1.2.0 // indirect
- github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88
+ github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3
github.com/onsi/ginkgo v1.15.0 // indirect
github.com/onsi/gomega v1.10.5 // indirect
github.com/sirupsen/logrus v1.8.0
diff --git a/go.sum b/go.sum
@@ -107,6 +107,12 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88 h1:YJ//HmHOYJ4srm/LA6VPNjNisneMbY6TTM1xttV/ZQU=
github.com/gotosocial/oauth2/v4 v4.2.1-0.20210316171520-7b12112bbb88/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8=
+github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132047-b7df44000ea6 h1:mWWMTK2Boy6FSCi45WB6GVCcXW3IoTVJKJiHmmdjywU=
+github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132047-b7df44000ea6/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8=
+github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132554-68b81fe90e62 h1:duqoA9NSY+BFY2IVveXx5lSvIQliVvPsaNMdspkTJPc=
+github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318132554-68b81fe90e62/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8=
+github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3 h1:CKRz5d7mRum+UMR88Ue33tCYcej14WjUsB59C02DDqY=
+github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
diff --git a/internal/api/server.go b/internal/api/server.go
@@ -19,6 +19,10 @@
package api
import (
+ "fmt"
+ "os"
+ "path/filepath"
+
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
@@ -71,6 +75,10 @@ func New(config *config.Config, logger *logrus.Logger) Server {
engine := gin.New()
store := memstore.NewStore([]byte("authentication-key"), []byte("encryption-keyencryption-key----"))
engine.Use(sessions.Sessions("gotosocial-session", store))
+ cwd, _ := os.Getwd()
+ tmPath := filepath.Join(cwd, fmt.Sprintf("%s*", config.TemplateConfig.BaseDir))
+ logger.Debugf("loading templates from %s", tmPath)
+ engine.LoadHTMLGlob(tmPath)
return &server{
APIGroup: engine.Group("/api").Group("/v1"),
logger: logger,
diff --git a/internal/config/config.go b/internal/config/config.go
@@ -27,25 +27,38 @@ import (
// Config pulls together all the configuration needed to run gotosocial
type Config struct {
- LogLevel string `yaml:"logLevel"`
- ApplicationName string `yaml:"applicationName"`
- DBConfig *DBConfig `yaml:"db"`
+ LogLevel string `yaml:"logLevel"`
+ ApplicationName string `yaml:"applicationName"`
+ DBConfig *DBConfig `yaml:"db"`
+ TemplateConfig *TemplateConfig `yaml:"template"`
}
-// New returns a new config, or an error if something goes amiss.
-// The path parameter is optional, for loading a configuration json from the given path.
-func New(path string) (*Config, error) {
- config := &Config{
- DBConfig: &DBConfig{},
+// FromFile returns a new config from a file, or an error if something goes amiss.
+func FromFile(path string) (*Config, error) {
+ c, err := loadFromFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("error creating config: %s", err)
}
- if path != "" {
- var err error
- if config, err = loadFromFile(path); err != nil {
- return nil, fmt.Errorf("error creating config: %s", err)
- }
+ return c, nil
+}
+
+// Default returns a new config with default values.
+// Not yet implemented.
+func Default() *Config {
+ // TODO: find a way of doing this without code repetition, because having to
+ // repeat all values here and elsewhere is annoying and gonna be prone to mistakes.
+ return &Config{
+ DBConfig: &DBConfig{},
+ TemplateConfig: &TemplateConfig{},
}
+}
- return config, nil
+// Empty just returns an empty config
+func Empty() *Config {
+ return &Config{
+ DBConfig: &DBConfig{},
+ TemplateConfig: &TemplateConfig{},
+ }
}
// loadFromFile takes a path to a yaml file and attempts to load a Config object from it
@@ -63,8 +76,8 @@ func loadFromFile(path string) (*Config, error) {
return config, nil
}
-// ParseFlags sets flags on the config using the provided Flags object
-func (c *Config) ParseFlags(f KeyedFlags) {
+// ParseCLIFlags sets flags on the config using the provided Flags object
+func (c *Config) ParseCLIFlags(f KeyedFlags) {
fn := GetFlagNames()
// For all of these flags, we only want to set them on the config if:
@@ -108,6 +121,11 @@ func (c *Config) ParseFlags(f KeyedFlags) {
if c.DBConfig.Database == "" || f.IsSet(fn.DbDatabase) {
c.DBConfig.Database = f.String(fn.DbDatabase)
}
+
+ // template flags
+ if c.TemplateConfig.BaseDir == "" || f.IsSet(fn.TemplateBaseDir) {
+ c.TemplateConfig.BaseDir = f.String(fn.TemplateBaseDir)
+ }
}
// KeyedFlags is a wrapper for any type that can store keyed flags and give them back.
@@ -130,6 +148,7 @@ type Flags struct {
DbUser string
DbPassword string
DbDatabase string
+ TemplateBaseDir string
}
// GetFlagNames returns a struct containing the names of the various flags used for
@@ -145,6 +164,7 @@ func GetFlagNames() Flags {
DbUser: "db-user",
DbPassword: "db-password",
DbDatabase: "db-database",
+ TemplateBaseDir: "template-basedir",
}
}
@@ -161,5 +181,6 @@ func GetEnvNames() Flags {
DbUser: "GTS_DB_USER",
DbPassword: "GTS_DB_PASSWORD",
DbDatabase: "GTS_DB_DATABASE",
+ TemplateBaseDir: "GTS_TEMPLATE_BASEDIR",
}
}
diff --git a/internal/config/template.go b/internal/config/template.go
@@ -0,0 +1,25 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package config
+
+// TemplateConfig pertains to templating of web pages/email notifications and the like
+type TemplateConfig struct {
+ // Directory from which gotosocial will attempt to load html templates (.tmpl files).
+ BaseDir string `yaml:"baseDir"`
+}
diff --git a/internal/oauth/html.go b/internal/oauth/html.go
@@ -2,67 +2,8 @@ package oauth
const (
signInHTML = `
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <title>Login</title>
- <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
- <script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
- <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
-</head>
-
-<body>
- <div class="container">
- <h1>Login</h1>
- <form action="/auth/sign_in" method="POST">
- <div class="form-group">
- <label for="email">Email</label>
- <input type="text" class="form-control" name="username" required placeholder="Please enter your email address">
- </div>
- <div class="form-group">
- <label for="password">Password</label>
- <input type="password" class="form-control" name="password" placeholder="Please enter your password">
- </div>
- <button type="submit" class="btn btn-success">Login</button>
- </form>
- </div>
-</body>
-
-</html>`
+`
authorizeHTML = `
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <title>Auth</title>
- <link
- rel="stylesheet"
- href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
- />
- <script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
- <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
- </head>
-
- <body>
- <div class="container">
- <div class="jumbotron">
- <form action="/oauth/authorize" method="POST">
- <h1>Authorize</h1>
- <p>The client would like to perform actions on your behalf.</p>
- <p>
- <button
- type="submit"
- class="btn btn-primary btn-lg"
- style="width:200px;"
- >
- Allow
- </button>
- </p>
- </form>
- </div>
- </div>
- </body>
-</html>`
+`
)
diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go
@@ -27,6 +27,7 @@ import (
"github.com/go-pg/pg/v10"
"github.com/gotosocial/gotosocial/internal/api"
"github.com/gotosocial/gotosocial/internal/gtsmodel"
+ "github.com/gotosocial/gotosocial/pkg/mastotypes"
"github.com/gotosocial/oauth2/v4"
"github.com/gotosocial/oauth2/v4/errors"
"github.com/gotosocial/oauth2/v4/manage"
@@ -45,16 +46,12 @@ type API struct {
}
type login struct {
- Email string `form:"username"`
+ Email string `form:"username"`
Password string `form:"password"`
}
-type authorize struct {
- ForceLogin string `form:"force_login,omitempty"`
- ResponseType string `form:"response_type"`
- ClientID string `form:"client_id"`
- RedirectURI string `form:"redirect_uri"`
- Scope string `form:"scope,omitempty"`
+type code struct {
+ Code string `form:"code"`
}
func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.Logger) *API {
@@ -79,6 +76,9 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L
oauth2.AuthorizationCode,
oauth2.Refreshing,
},
+ AllowedCodeChallengeMethods: []oauth2.CodeChallengeMethod{
+ oauth2.CodeChallengePlain,
+ },
}
srv := server.NewServer(sc, manager)
@@ -106,9 +106,13 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L
func (a *API) AddRoutes(s api.Server) error {
s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler)
s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler)
+
s.AttachHandler(http.MethodPost, "/oauth/token", a.TokenHandler)
+
s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeGETHandler)
- s.AttachHandler(methodAny, "/auth", a.AuthHandler)
+ s.AttachHandler(http.MethodPost, "/oauth/authorize", a.AuthorizePOSTHandler)
+
+ // s.AttachHandler(http.MethodGet, "/auth", a.AuthGETHandler)
return nil
}
@@ -125,7 +129,7 @@ func incorrectPassword() (string, error) {
// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler
func (a *API) SignInGETHandler(c *gin.Context) {
a.log.WithField("func", "SignInGETHandler").Trace("serving sign in html")
- c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(signInHTML))
+ c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
}
// SignInPOSTHandler should be served at https://example.org/auth/sign_in.
@@ -135,15 +139,16 @@ func (a *API) SignInPOSTHandler(c *gin.Context) {
l := a.log.WithField("func", "SignInPOSTHandler")
s := sessions.Default(c)
form := &login{}
- if err := c.ShouldBind(form); err != nil || form.Email == "" || form.Password == "" {
+ if err := c.ShouldBind(form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
l.Tracef("parsed form: %+v", form)
- userid, err := a.ValidatePassword(form.Email, form.Password);
+ userid, err := a.ValidatePassword(form.Email, form.Password)
if err != nil {
c.String(http.StatusForbidden, err.Error())
+ return
}
s.Set("username", userid)
@@ -151,11 +156,12 @@ func (a *API) SignInPOSTHandler(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
+
l.Trace("redirecting to auth page")
- c.Redirect(http.StatusFound, "/auth")
+ c.Redirect(http.StatusFound, "/oauth/authorize")
}
-// TokenHandler should be served at https://example.org/oauth/token
+// TokenHandler should be served as a POST at https://example.org/oauth/token
// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
// See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token
func (a *API) TokenHandler(c *gin.Context) {
@@ -166,50 +172,61 @@ func (a *API) TokenHandler(c *gin.Context) {
}
}
-// AuthorizeHandler should be served as GET at https://example.org/oauth/authorize
+// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
// The idea here is to present an oauth authorize page to the user, with a button
// that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user
func (a *API) AuthorizeGETHandler(c *gin.Context) {
- l := a.log.WithField("func", "AuthorizeHandler")
+ l := a.log.WithField("func", "AuthorizeGETHandler")
s := sessions.Default(c)
- form := &authorize{}
-
- if err := c.ShouldBind(form); err != nil {
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
- return
- }
- l.Tracef("parsed form: %+v", form)
-
- if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" {
- c.JSON(http.StatusBadRequest, gin.H{"error": "missing one of: response_type, client_id or redirect_uri"})
- return
- }
-
- s.Set("force_login", form.ForceLogin)
- s.Set("response_type", form.ResponseType)
- s.Set("client_id", form.ClientID)
- s.Set("redirect_uri", form.RedirectURI)
- s.Set("scope", form.Scope)
- if err := s.Save(); err != nil {
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
- }
v := s.Get("username")
if username, ok := v.(string); !ok || username == "" {
- l.Trace("username was empty, redirecting to sign in page")
+ l.Trace("username was empty, parsing form then redirecting to sign in page")
+
+ form := &mastotypes.OAuthAuthorize{}
+
+ if err := c.ShouldBind(form); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+ l.Tracef("parsed form: %+v", form)
+
+ if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "missing one of: response_type, client_id or redirect_uri"})
+ return
+ }
+
+ // save these values from the form so we can use them elsewhere in the session
+ s.Set("force_login", form.ForceLogin)
+ s.Set("response_type", form.ResponseType)
+ s.Set("client_id", form.ClientID)
+ s.Set("redirect_uri", form.RedirectURI)
+ s.Set("scope", form.Scope)
+ if err := s.Save(); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
c.Redirect(http.StatusFound, "/auth/sign_in")
return
}
l.Trace("serving authorize html")
- c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(authorizeHTML))
+ c.HTML(http.StatusOK, "authorize.tmpl", gin.H{})
}
-// AuthHandler should be served at https://example.org/auth
-func (a *API) AuthHandler(c *gin.Context) {
- l := a.log.WithField("func", "AuthHandler")
+// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
+// The idea here is to present an oauth authorize page to the user, with a button
+// that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user
+func (a *API) AuthorizePOSTHandler(c *gin.Context) {
+ l := a.log.WithField("func", "AuthorizePOSTHandler")
s := sessions.Default(c)
+ v := s.Get("username")
+ if username, ok := v.(string); !ok || username == "" {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "you are not signed in"})
+ }
+
values := url.Values{}
if v, ok := s.Get("force_login").(string); !ok {
@@ -277,7 +294,13 @@ func (a *API) AuthHandler(c *gin.Context) {
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
func (a *API) ValidatePassword(email string, password string) (userid string, err error) {
l := a.log.WithField("func", "PasswordAuthorizationHandler")
- l.Tracef("email %s password %s", email, password)
+
+ // make sure an email/password was provided and bail if not
+ if email == "" || password == "" {
+ l.Debug("email or password was not provided")
+ return incorrectPassword()
+ }
+
// first we select the user from the database based on email address, bail if no user found for that email
gtsUser := >smodel.User{}
if err := a.conn.Model(gtsUser).Where("email = ?", email).Select(); err != nil {
@@ -297,8 +320,7 @@ func (a *API) ValidatePassword(email string, password string) (userid string, er
return incorrectPassword()
}
- // If we've made it this far the email/password is correct so we need the oauth client-id of the user
- // This is, conveniently, the same as the user ID, so we can just return it.
+ // If we've made it this far the email/password is correct, so we can just return the id of the user.
userid = gtsUser.ID
l.Tracef("returning (%s, %s)", userid, err)
return
diff --git a/internal/oauth/oauth_test.go b/internal/oauth/oauth_test.go
@@ -40,17 +40,24 @@ func (suite *OauthTestSuite) SetupSuite() {
suite.testUser = >smodel.User{
ID: userID,
EncryptedPassword: string(encryptedPassword),
- Email: "user@localhost",
+ Email: "user@example.org",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
- AccountID: "some-account-id-it-doesn't-matter-really",
+ AccountID: "some-account-id-it-doesn't-matter-really-since-this-user-doesn't-actually-have-an-account!",
}
suite.testClient = &oauthClient{
ID: "a-known-client-id",
Secret: "some-secret",
- Domain: "http://localhost:8080",
+ Domain: "https://example.org",
UserID: userID,
}
+
+ // because go tests are run within the test package directory, we need to fiddle with the templateconfig
+ // basedir in a way that we wouldn't normally have to do when running the binary, in order to make
+ // the templates actually load
+ c := config.Empty()
+ c.TemplateConfig.BaseDir = "../../web/template/"
+ suite.config = c
}
// SetupTest creates a postgres connection and creates the oauth_clients table before each test
@@ -114,7 +121,7 @@ func (suite *OauthTestSuite) TestAPIInitialize() {
api.AddRoutes(r)
go r.Start()
time.Sleep(30 * time.Second)
- // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&redirect_uri=''&response_type=code
+ // http://localhost:8080/oauth/authorize?client_id=a-known-client-id&response_type=code&redirect_uri=https://example.org
}
func TestOauthTestSuite(t *testing.T) {
diff --git a/pkg/mastotypes/oauth.go b/pkg/mastotypes/oauth.go
@@ -0,0 +1,37 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package mastotypes
+
+// OAuthAuthorize represents a request sent to https://example.org/oauth/authorize
+// See here: https://docs.joinmastodon.org/methods/apps/oauth/
+type OAuthAuthorize struct {
+ // Forces the user to re-login, which is necessary for authorizing with multiple accounts from the same instance.
+ ForceLogin string `form:"force_login,omitempty"`
+ // Should be set equal to `code`.
+ ResponseType string `form:"response_type"`
+ // Client ID, obtained during app registration.
+ ClientID string `form:"client_id"`
+ // Set a URI to redirect the user to.
+ // If this parameter is set to urn:ietf:wg:oauth:2.0:oob then the authorization code will be shown instead.
+ // Must match one of the redirect URIs declared during app registration.
+ RedirectURI string `form:"redirect_uri"`
+ // List of requested OAuth scopes, separated by spaces (or by pluses, if using query parameters).
+ // Must be a subset of scopes declared during app registration. If not provided, defaults to read.
+ Scope string `form:"scope,omitempty"`
+}
diff --git a/web/template/authorize.tmpl b/web/template/authorize.tmpl
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <title>Auth</title>
+ <link
+ rel="stylesheet"
+ href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
+ />
+ <script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
+ </head>
+
+ <body>
+ <div class="container">
+ <div class="jumbotron">
+ <form action="/oauth/authorize" method="POST">
+ <h1>Authorize</h1>
+ <p>The client would like to perform actions on your behalf.</p>
+ <p>
+ <button
+ type="submit"
+ class="btn btn-primary btn-lg"
+ style="width:200px;"
+ >
+ Allow
+ </button>
+ </p>
+ </form>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/web/template/sign-in.tmpl b/web/template/sign-in.tmpl
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Login</title>
+ <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
+ <script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
+ <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
+</head>
+
+<body>
+ <div class="container">
+ <h1>Login</h1>
+ <form action="/auth/sign_in" method="POST">
+ <div class="form-group">
+ <label for="email">Email</label>
+ <input type="text" class="form-control" name="username" required placeholder="Please enter your email address">
+ </div>
+ <div class="form-group">
+ <label for="password">Password</label>
+ <input type="password" class="form-control" name="password" placeholder="Please enter your password">
+ </div>
+ <button type="submit" class="btn btn-success">Login</button>
+ </form>
+ </div>
+</body>
+
+</html>