Fix auth and start configuring monaco

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main
jolheiser 2022-07-10 23:40:18 -05:00
parent 9065e9684b
commit 99cd67d90b
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
13 changed files with 295 additions and 160 deletions

View File

@ -43,7 +43,7 @@ func main() {
log.Fatal().Err(err).Msg("")
}
r := router.New(router.Config{
r := router.New(router.App{
Domain: *domainFlag,
SessionSecret: *sessionSecretFlag,
GiteaURL: *giteaURLFlag,

32
router/app.go 100644
View File

@ -0,0 +1,32 @@
package router
import (
"io"
"net/http"
"go.jolheiser.com/gistea/router/session"
"go.jolheiser.com/gistea/static"
)
type App struct {
Domain string
SessionSecret string
GiteaURL string
GiteaClientKey string
GiteaClientSecret string
store *session.Store
}
func (a *App) Tmpl(w io.Writer, r *http.Request, page static.Page, data map[string]any) error {
info, _ := a.store.Info(r)
ctx := static.Context{
"Domain": a.Domain,
"GiteaURL": a.GiteaURL,
"Page": page,
"Session": info,
"Data": data,
}
return static.Tmpl(w, static.Base, ctx)
}

22
router/gist.go 100644
View File

@ -0,0 +1,22 @@
package router
import (
"net/http"
"go.jolheiser.com/gistea/static"
)
func (a *App) newGist(w http.ResponseWriter, r *http.Request) {
page := static.New
repos, err := a.store.Repos(r)
if err != nil {
page = static.Landing
}
if err := a.Tmpl(w, r, page, map[string]any{
"Repos": repos,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

34
router/oauth.go 100644
View File

@ -0,0 +1,34 @@
package router
import (
"net/http"
"github.com/markbates/goth/gothic"
)
func (a *App) login(w http.ResponseWriter, r *http.Request) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
gothic.BeginAuthHandler(w, r)
return
}
if err := a.store.CompleteAuth(w, r, user.AccessToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, a.Domain, http.StatusFound)
}
func (a *App) loginCallback(w http.ResponseWriter, r *http.Request) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if err := a.store.CompleteAuth(w, r, user.AccessToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, a.Domain, http.StatusFound)
}

View File

@ -5,6 +5,7 @@ import (
"net/http"
"strings"
"go.jolheiser.com/gistea/router/session"
"go.jolheiser.com/gistea/static"
"github.com/go-chi/chi/v5"
@ -16,35 +17,27 @@ import (
"github.com/rs/zerolog/log"
)
type Config struct {
Domain string
SessionSecret string
GiteaURL string
GiteaClientKey string
GiteaClientSecret string
}
func New(cfg Config) *chi.Mux {
cfg.GiteaURL = strings.TrimRight(cfg.GiteaURL, "/")
cfg.Domain = strings.TrimRight(cfg.Domain, "/")
callbackURL := fmt.Sprintf("%s/auth/callback", cfg.Domain)
func New(app App) *chi.Mux {
app.GiteaURL = strings.TrimRight(app.GiteaURL, "/")
app.Domain = strings.TrimRight(app.Domain, "/")
callbackURL := fmt.Sprintf("%s/_/auth/callback", app.Domain)
goth.UseProviders(
gitea.NewCustomisedURL(cfg.GiteaClientKey, cfg.GiteaClientSecret, callbackURL,
fmt.Sprintf("%s/login/oauth/authorize", cfg.GiteaURL),
fmt.Sprintf("%s/login/oauth/access_token", cfg.GiteaURL),
fmt.Sprintf("%s/api/v1/user", cfg.GiteaURL),
gitea.NewCustomisedURL(app.GiteaClientKey, app.GiteaClientSecret, callbackURL,
fmt.Sprintf("%s/login/oauth/authorize", app.GiteaURL),
fmt.Sprintf("%s/login/oauth/access_token", app.GiteaURL),
fmt.Sprintf("%s/api/v1/user", app.GiteaURL),
),
)
gothStore := sessions.NewCookieStore([]byte(cfg.GiteaClientSecret))
gothStore := sessions.NewCookieStore([]byte(app.GiteaClientSecret))
gothStore.Options.HttpOnly = true
gothic.Store = gothStore
gothic.GetProviderName = func(_ *http.Request) (string, error) {
return "gitea", nil
}
store := NewSessionStore(cfg.SessionSecret, cfg.GiteaURL)
store := session.NewStore(app.SessionSecret, app.GiteaURL)
app.store = store
r := chi.NewMux()
r.Use(middleware.Logger)
@ -55,50 +48,18 @@ func New(cfg Config) *chi.Mux {
NoColor: true,
})
r.Route("/", func(r chi.Router) {
r.Use(store.Middleware)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
repos, err := store.Repos(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
r.Route("/_", func(r chi.Router) {
r.Mount("/css", static.CSS)
r.Mount("/js", static.JS)
if err := static.Tmpl(w, static.Base, static.Context{
"repos": repos,
"page": static.New,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
r.Route("/auth", func(r chi.Router) {
r.Get("/login", app.login)
r.Get("/callback", app.loginCallback)
})
})
r.Route("/auth", func(r chi.Router) {
r.Get("/login", func(w http.ResponseWriter, r *http.Request) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
gothic.BeginAuthHandler(w, r)
return
}
if err := store.Auth(w, r, user.AccessToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, cfg.Domain, http.StatusFound)
})
r.Get("/callback", func(w http.ResponseWriter, r *http.Request) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if err := store.Auth(w, r, user.AccessToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, cfg.Domain, http.StatusFound)
})
r.Route("/", func(r chi.Router) {
r.Get("/", app.newGist)
})
return r

View File

@ -1,6 +1,8 @@
package router
package session
import (
"encoding/gob"
"errors"
"fmt"
"net/http"
@ -9,14 +11,26 @@ import (
"github.com/markbates/goth/gothic"
)
const sessionCookie = "_gistea_session"
const (
sessionCookie = "_gistea_session"
infoKey = "_gistea_info"
)
type SessionStore struct {
type Store struct {
Store sessions.Store
GiteaURL string
}
func (s *SessionStore) Middleware(next http.Handler) http.Handler {
type Info struct {
Org string
Token string
}
func init() {
gob.Register(&Info{})
}
func (s *Store) RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sess, err := s.Store.Get(r, sessionCookie)
if err != nil {
@ -24,7 +38,7 @@ func (s *SessionStore) Middleware(next http.Handler) http.Handler {
return
}
if _, ok := sess.Values["authenticated"]; !ok {
if _, ok := sess.Values[infoKey]; !ok {
gothic.BeginAuthHandler(w, r)
return
}
@ -33,7 +47,25 @@ func (s *SessionStore) Middleware(next http.Handler) http.Handler {
})
}
func (s *SessionStore) Auth(w http.ResponseWriter, r *http.Request, token string) error {
func (s *Store) Info(r *http.Request) (*Info, error) {
sess, err := s.Store.Get(r, sessionCookie)
if err != nil {
return nil, err
}
if infoAny, ok := sess.Values[infoKey]; ok {
if info, ok := infoAny.(*Info); ok {
return info, nil
}
}
return nil, errors.New("could not get session info")
}
func (s *Store) HasAuth(r *http.Request) bool {
i, _ := s.Info(r)
return i != nil
}
func (s *Store) CompleteAuth(w http.ResponseWriter, r *http.Request, token string) error {
client, err := gitea.NewClient(s.GiteaURL, gitea.SetToken(token))
if err != nil {
return err
@ -63,39 +95,31 @@ func (s *SessionStore) Auth(w http.ResponseWriter, r *http.Request, token string
return err
}
sess.Values["authenticated"] = true
sess.Values["org"] = org
sess.Values["token"] = token
sess.Values[infoKey] = &Info{
Org: org,
Token: token,
}
return s.Store.Save(r, w, sess)
}
func (s *SessionStore) client(r *http.Request) (*gitea.Client, error) {
sess, err := s.Store.Get(r, sessionCookie)
func (s *Store) Repos(r *http.Request) ([]*gitea.Repository, error) {
info, err := s.Info(r)
if err != nil {
return nil, err
}
return gitea.NewClient(s.GiteaURL, gitea.SetToken(sess.Values["token"].(string)))
}
func (s *SessionStore) Repos(r *http.Request) ([]*gitea.Repository, error) {
sess, err := s.Store.Get(r, sessionCookie)
client, err := gitea.NewClient(s.GiteaURL, gitea.SetToken(info.Token))
if err != nil {
return nil, err
}
client, err := s.client(r)
if err != nil {
return nil, err
}
repos, _, err := client.ListOrgRepos(sess.Values["org"].(string), gitea.ListOrgReposOptions{})
repos, _, err := client.ListOrgRepos(info.Org, gitea.ListOrgReposOptions{})
return repos, err
}
func NewSessionStore(sessionSecret, giteURL string) *SessionStore {
func NewStore(sessionSecret, giteURL string) *Store {
store := sessions.NewCookieStore([]byte(sessionSecret))
return &SessionStore{
return &Store{
Store: store,
GiteaURL: giteURL,
}

View File

@ -0,0 +1,11 @@
.form-input {
border-radius: 10px;
margin: 1em 0;
}
.form-head {
border: black solid thin;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
padding: 0 1em;
}

View File

@ -0,0 +1,39 @@
require.config({paths: {'vs': 'https://unpkg.com/monaco-editor@latest/min/vs'}});
// Before loading vs/editor/editor.main, define a global MonacoEnvironment that overwrites
// the default worker url location (used when creating WebWorkers). The problem here is that
// HTML5 does not allow cross-domain web workers, so we need to proxy the instantiation of
// a web worker through a same-domain script
window.MonacoEnvironment = {
getWorkerUrl: function (workerId, label) {
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
self.MonacoEnvironment = {
baseUrl: 'https://unpkg.com/monaco-editor@latest/min/'
};
importScripts('https://unpkg.com/monaco-editor@latest/min/vs/base/worker/workerMain.js');`
)}`;
}
};
let editors = [];
require(["vs/editor/editor.main"], function () {
let $filenames = document.querySelectorAll(".monaco-filename");
document.querySelectorAll('.monaco-editor').forEach((elem, idx) => {
let $editor = monaco.editor.create(elem, {
language: 'text',
theme: 'vs-dark',
});
elem.classList.remove("loading", "loading-lg");
let $filename = $filenames[idx];
editors.push({
"filename": $filename,
"editor": $editor
});
$filename.addEventListener('change', () => {
monaco.editor.setModelLanguage($editor.getModel(), $filename.value.split(".").pop());
});
})
//monaco.editor.setModelLanguage($editor.getModel(), "go");
});

View File

@ -1,49 +1,16 @@
package static
import (
"bytes"
_ "embed"
"fmt"
"html/template"
"io"
"github.com/rs/zerolog/log"
"embed"
"net/http"
)
type Context = map[string]any
var (
templates = make(map[string]*template.Template)
//go:embed css
css embed.FS
CSS = http.StripPrefix("/_", http.FileServer(http.FS(css)))
//go:embed templates/base.tmpl
baseTmpl string
Base = "base"
//go:embed templates/new.tmpl
newTmpl string
New = "new"
//go:embed js
js embed.FS
JS = http.StripPrefix("/_", http.FileServer(http.FS(js)))
)
func init() {
templates[Base] = template.Must(template.New("").Funcs(funcMap).Parse(baseTmpl))
templates[New] = template.Must(template.New("").Funcs(funcMap).Parse(newTmpl))
}
func Tmpl(w io.Writer, name string, ctx any) error {
if tmpl, ok := templates[name]; ok {
return tmpl.Execute(w, ctx)
}
return fmt.Errorf("unknown template %q", name)
}
var funcMap = template.FuncMap{
"tmpl": func(name string, ctx any) template.HTML {
var buf bytes.Buffer
err := Tmpl(&buf, name, ctx)
if err != nil {
log.Err(err).Msg("")
return ""
}
return template.HTML(buf.String())
},
}

View File

@ -0,0 +1,56 @@
package static
import (
"bytes"
_ "embed"
"fmt"
"html/template"
"io"
"github.com/rs/zerolog/log"
)
type Context map[string]any
type Page string
var (
templates = make(map[Page]*template.Template)
//go:embed templates/base.tmpl
baseTmpl string
Base Page = "base"
//go:embed templates/new.tmpl
newTmpl string
New Page = "new"
//go:embed templates/landing.tmpl
landingTmpl string
Landing Page = "landing"
)
func init() {
templates[Base] = template.Must(template.New("").Funcs(funcMap).Parse(baseTmpl))
templates[New] = template.Must(template.New("").Funcs(funcMap).Parse(newTmpl))
templates[Landing] = template.Must(template.New("").Funcs(funcMap).Parse(landingTmpl))
}
func Tmpl(w io.Writer, name Page, ctx Context) error {
if tmpl, ok := templates[name]; ok {
return tmpl.Execute(w, ctx)
}
return fmt.Errorf("unknown template %q", name)
}
var funcMap = template.FuncMap{
"tmpl": func(name Page, ctx Context) template.HTML {
var buf bytes.Buffer
err := Tmpl(&buf, name, ctx)
if err != nil {
log.Err(err).Msg("")
return ""
}
return template.HTML(buf.String())
},
}

View File

@ -6,19 +6,25 @@
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre.min.css">
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre-exp.min.css">
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre-icons.min.css">
<link rel="stylesheet" href="/_/css/gistea.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<body class="bg-dark">
<div class="container">
<header class="navbar">
<section class="navbar-section">
<a href="/" class="navbar-brand mr-2">Gistea</a>
<a href="/" class="btn btn-link">Back to Gitea</a>
<a href="/" class="btn btn-link">Your gists</a>
<a href="{{.Domain}}" class="navbar-brand mr-2">Gistea</a>
<a href="{{.GiteaURL}}" class="btn btn-link">Back to Gitea</a>
{{if .Session}}<a href="/" class="btn btn-link">Your gists</a>{{end}}
</section>
{{if not .Session}}
<section class="navbar-section">
<a href="{{.Domain}}/_/auth/login" class="navbar-brand mr-2">Login</a>
</section>
{{end}}
</header>
<div class="divider"></div>
{{if .page}}{{tmpl .page .}}{{end}}
{{tmpl .Page .}}
</div>
</body>
</html>

View File

@ -0,0 +1,7 @@
<div class="container">
<div class="columns">
<div class="column col-6 col-mx-auto">
Please login
</div>
</div>
</div>

View File

@ -1,43 +1,19 @@
<div class="container">
<div class="columns">
<div class="column col-6 col-mx-auto">
<input class="form-input" type="text" id="description" placeholder="Gist description">
<input class="form-input bg-dark" type="text" id="description" placeholder="Gist description">
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-6 col-mx-auto">
<div id="monaco-editor" style="height: 20em"></div>
<div class="form-head">
<input class="monaco-filename form-input bg-dark col-6" type="text" placeholder="Filename (with extension)">
</div>
<div class="monaco-editor loading loading-lg" style="height: 20em"></div>
</div>
</div>
</div>
<script type="text/javascript" src="https://unpkg.com/monaco-editor@latest/min/vs/loader.js"></script>
<script>
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@latest/min/vs' }});
// Before loading vs/editor/editor.main, define a global MonacoEnvironment that overwrites
// the default worker url location (used when creating WebWorkers). The problem here is that
// HTML5 does not allow cross-domain web workers, so we need to proxy the instantiation of
// a web worker through a same-domain script
window.MonacoEnvironment = {
getWorkerUrl: function(workerId, label) {
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
self.MonacoEnvironment = {
baseUrl: 'https://unpkg.com/monaco-editor@latest/min/'
};
importScripts('https://unpkg.com/monaco-editor@latest/min/vs/base/worker/workerMain.js');`
)}`;
}
};
require(["vs/editor/editor.main"], function () {
monaco.editor.create(document.querySelector('#monaco-editor'), {
value: `function x() {
console.log("Hello world!");
}`,
language: 'html',
theme: 'vs-dark',
});
});
</script>
<script src="{{.Domain}}/_/js/monaco.js"></script>