diff --git a/account.go b/account.go index d32f503..42e9982 100644 --- a/account.go +++ b/account.go @@ -311,6 +311,8 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { OauthWriteAs bool OauthGitlab bool GitlabDisplayName string + OauthGitea bool + GiteaDisplayName string }{ pageForReq(app, r), r.FormValue("to"), @@ -321,6 +323,8 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { app.Config().WriteAsOauth.ClientID != "", app.Config().GitlabOauth.ClientID != "", config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), + app.Config().GiteaOauth.ClientID != "", + config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), } if earlyError != "" { @@ -1046,6 +1050,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err enableOauthSlack := app.Config().SlackOauth.ClientID != "" enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != "" enableOauthGitLab := app.Config().GitlabOauth.ClientID != "" + enableOauthGitea := app.Config().GiteaOauth.ClientID != "" oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID) if err != nil { @@ -1060,10 +1065,12 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err enableOauthWriteAs = false case "gitlab": enableOauthGitLab = false + case "gitea": + enableOauthGitea = false } } - displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || len(oauthAccounts) > 0 + displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGitea || len(oauthAccounts) > 0 obj := struct { *UserPage @@ -1077,6 +1084,8 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err OauthWriteAs bool OauthGitLab bool GitLabDisplayName string + OauthGitea bool + GiteaDisplayName string }{ UserPage: NewUserPage(app, r, u, "Account Settings", flashes), Email: fullUser.EmailClear(app.keys), @@ -1089,6 +1098,8 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err OauthWriteAs: enableOauthWriteAs, OauthGitLab: enableOauthGitLab, GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), + OauthGitea: enableOauthGitea, + GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), } showUserPage(w, "settings", obj) diff --git a/config/config.go b/config/config.go index 520dd59..7ccdfba 100644 --- a/config/config.go +++ b/config/config.go @@ -86,6 +86,15 @@ type ( CallbackProxyAPI string `ini:"callback_proxy_api"` } + GiteaOauthCfg struct { + ClientID string `ini:"client_id"` + ClientSecret string `ini:"client_secret"` + Host string `ini:"host"` + DisplayName string `ini:"display_name"` + CallbackProxy string `ini:"callback_proxy"` + CallbackProxyAPI string `ini:"callback_proxy_api"` + } + // AppCfg holds values that affect how the application functions AppCfg struct { SiteName string `ini:"site_name"` @@ -138,6 +147,7 @@ type ( SlackOauth SlackOauthCfg `ini:"oauth.slack"` WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` + GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"` } ) diff --git a/oauth.go b/oauth.go index 9073f75..902c31b 100644 --- a/oauth.go +++ b/oauth.go @@ -208,6 +208,34 @@ func configureGitlabOauth(parentHandler *Handler, r *mux.Router, app *App) { } } +func configureGiteaOauth(parentHandler *Handler, r *mux.Router, app *App) { + if app.Config().GiteaOauth.ClientID != "" { + callbackLocation := app.Config().App.Host + "/oauth/callback/gitea" + + var callbackProxy *callbackProxyClient = nil + if app.Config().GiteaOauth.CallbackProxy != "" { + callbackProxy = &callbackProxyClient{ + server: app.Config().GiteaOauth.CallbackProxyAPI, + callbackLocation: app.Config().App.Host + "/oauth/callback/gitea", + httpClient: config.DefaultHTTPClient(), + } + callbackLocation = app.Config().GiteaOauth.CallbackProxy + } + + address := config.OrDefaultString(app.Config().GiteaOauth.Host, giteaHost) + oauthClient := giteaOauthClient{ + ClientID: app.Config().GiteaOauth.ClientID, + ClientSecret: app.Config().GiteaOauth.ClientSecret, + ExchangeLocation: address + "/login/oauth/access_token", + InspectLocation: address + "/api/v1/user", + AuthLocation: address + "/login/oauth/authorize", + HttpClient: config.DefaultHTTPClient(), + CallbackLocation: callbackLocation, + } + configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) + } +} + func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauthClient oauthClient, callbackProxy *callbackProxyClient) { handler := &oauthHandler{ Config: app.Config(), diff --git a/oauth_gitea.go b/oauth_gitea.go new file mode 100644 index 0000000..42331bf --- /dev/null +++ b/oauth_gitea.go @@ -0,0 +1,115 @@ +package writefreely + +import ( + "context" + "errors" + "net/http" + "net/url" + "strings" +) + +type giteaOauthClient struct { + ClientID string + ClientSecret string + AuthLocation string + ExchangeLocation string + InspectLocation string + CallbackLocation string + HttpClient HttpClient +} + +var _ oauthClient = giteaOauthClient{} + +const ( + giteaHost = "https://source.gyt.is" + giteaDisplayName = "Gitea" +) + +func (c giteaOauthClient) GetProvider() string { + return "gitea" +} + +func (c giteaOauthClient) GetClientID() string { + return c.ClientID +} + +func (c giteaOauthClient) GetCallbackLocation() string { + return c.CallbackLocation +} + +func (c giteaOauthClient) buildLoginURL(state string) (string, error) { + u, err := url.Parse(c.AuthLocation) + if err != nil { + return "", err + } + q := u.Query() + q.Set("client_id", c.ClientID) + q.Set("redirect_uri", c.CallbackLocation) + q.Set("response_type", "code") + q.Set("state", state) + // q.Set("scope", "read_user") + u.RawQuery = q.Encode() + return u.String(), nil +} + +func (c giteaOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { + form := url.Values{} + form.Add("grant_type", "authorization_code") + form.Add("redirect_uri", c.CallbackLocation) + // form.Add("scope", "read_user") + form.Add("code", code) + req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.WithContext(ctx) + req.Header.Set("User-Agent", "writefreely") + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(c.ClientID, c.ClientSecret) + + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("unable to exchange code for access token") + } + + var tokenResponse TokenResponse + if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil { + return nil, err + } + if tokenResponse.Error != "" { + return nil, errors.New(tokenResponse.Error) + } + return &tokenResponse, nil +} + +func (c giteaOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) { + req, err := http.NewRequest("GET", c.InspectLocation, nil) + if err != nil { + return nil, err + } + req.WithContext(ctx) + req.Header.Set("User-Agent", "writefreely") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+accessToken) + + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("unable to inspect access token") + } + + var inspectResponse InspectResponse + if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil { + return nil, err + } + if inspectResponse.Error != "" { + return nil, errors.New(inspectResponse.Error) + } + return &inspectResponse, nil +} diff --git a/pages/login.tmpl b/pages/login.tmpl index 94087f3..c09b16e 100644 --- a/pages/login.tmpl +++ b/pages/login.tmpl @@ -36,6 +36,10 @@ hr.short { box-sizing: border-box; font-size: 17px; } +#gitea-login { + box-sizing: border-box; + font-size: 17px; +} {{end}} {{define "content"}} @@ -46,7 +50,7 @@ hr.short { {{range .Flashes}}
  • {{.}}
  • {{end}} {{end}} - {{ if or .OauthSlack .OauthWriteAs .OauthGitlab }} + {{ if or .OauthSlack .OauthWriteAs .OauthGitlab .OauthGitea }}
    {{ if .OauthSlack }} Sign in with Slack @@ -57,6 +61,9 @@ hr.short { {{ if .OauthGitlab }} Sign in with {{.GitlabDisplayName}} {{ end }} + {{ if .OauthGitea }} + Sign in with {{.GiteaDisplayName}} + {{ end }}
    diff --git a/routes.go b/routes.go index b34bd3d..a6efa35 100644 --- a/routes.go +++ b/routes.go @@ -76,6 +76,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { configureSlackOauth(handler, write, apper.App()) configureWriteAsOauth(handler, write, apper.App()) configureGitlabOauth(handler, write, apper.App()) + configureGiteaOauth(handler, write, apper.App()) // Set up dyamic page handlers // Handle auth diff --git a/static/img/mark/gitea.png b/static/img/mark/gitea.png new file mode 100644 index 0000000..f58bb62 Binary files /dev/null and b/static/img/mark/gitea.png differ diff --git a/templates/user/settings.tmpl b/templates/user/settings.tmpl index 8ade8ad..1808881 100644 --- a/templates/user/settings.tmpl +++ b/templates/user/settings.tmpl @@ -93,7 +93,7 @@ h3 { font-weight: normal; } {{ end }}
    {{ end }} - {{ if or .OauthSlack .OauthWriteAs .OauthGitLab }} + {{ if or .OauthSlack .OauthWriteAs .OauthGitLab .OauthGitea }}

    Link External Accounts

    Connect additional accounts to enable logging in with those providers, instead of using your username and password.

    @@ -122,6 +122,14 @@ h3 { font-weight: normal; }
    {{ end }} + {{ if .OauthGitea }} +
    + Gitea + + Link {{.GiteaDisplayName}} + +
    + {{ end }} {{ end }}