Fix auth and start configuring monaco
Signed-off-by: jolheiser <john.olheiser@gmail.com>main
parent
9065e9684b
commit
99cd67d90b
2
main.go
2
main.go
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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");
|
||||
});
|
|
@ -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())
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
},
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column col-6 col-mx-auto">
|
||||
Please login
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue