Inject register & version information into index.html

The registration form will always be shown inside the dev mode,
because there is no api that transmits if registration is enabled.
This commit is contained in:
Jannis Mattheis 2021-08-02 13:02:17 +02:00
parent c172590b92
commit 36eb8d8b2b
8 changed files with 68 additions and 57 deletions

View File

@ -55,7 +55,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
userChangeNotifier.OnUserDeleted(pluginManager.RemoveUser) userChangeNotifier.OnUserDeleted(pluginManager.RemoveUser)
userChangeNotifier.OnUserAdded(pluginManager.InitializeForUserID) userChangeNotifier.OnUserAdded(pluginManager.InitializeForUserID)
ui.Register(g) ui.Register(g, *vInfo, conf.Registration)
g.GET("/health", healthHandler.Health) g.GET("/health", healthHandler.Health)
g.GET("/swagger", docs.Serve) g.GET("/swagger", docs.Serve)

View File

@ -60,6 +60,7 @@ rules:
jest/expect-expect: off jest/expect-expect: off
jest/no-jasmine-globals: off jest/no-jasmine-globals: off
"@typescript-eslint/require-await": off "@typescript-eslint/require-await": off
"@typescript-eslint/restrict-template-expressions": off
"@typescript-eslint/array-type": [error, {default: array-simple}] "@typescript-eslint/array-type": [error, {default: array-simple}]
"@typescript-eslint/await-thenable": error "@typescript-eslint/await-thenable": error

View File

@ -35,5 +35,8 @@
Gotify requires JavaScript. Gotify requires JavaScript.
</noscript> </noscript>
<div id="root"></div> <div id="root"></div>
<% if (process.env.NODE_ENV === 'production') { %>
<script>window.config = %CONFIG%;</script>
<% } %>
</body> </body>
</html> </html>

View File

@ -1,32 +1,51 @@
package ui package ui
import ( import (
"encoding/json"
"net/http" "net/http"
"strings"
"github.com/gin-contrib/gzip" "github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gobuffalo/packr/v2" "github.com/gobuffalo/packr/v2"
"github.com/gotify/server/v2/model"
) )
var box = packr.New("ui", "../ui/build") var box = packr.New("ui", "../ui/build")
type uiConfig struct {
Register bool `json:"register"`
Version model.VersionInfo `json:"version"`
}
// Register registers the ui on the root path. // Register registers the ui on the root path.
func Register(r *gin.Engine) { func Register(r *gin.Engine, version model.VersionInfo, register bool) {
uiConfigBytes, err := json.Marshal(uiConfig{Version: version, Register: register})
if err != nil {
panic(err)
}
ui := r.Group("/", gzip.Gzip(gzip.DefaultCompression)) ui := r.Group("/", gzip.Gzip(gzip.DefaultCompression))
ui.GET("/", serveFile("index.html", "text/html")) ui.GET("/", serveFile("index.html", "text/html", func(content string) string {
ui.GET("/index.html", serveFile("index.html", "text/html")) return strings.Replace(content, "%CONFIG%", string(uiConfigBytes), 1)
ui.GET("/manifest.json", serveFile("manifest.json", "application/json")) }))
ui.GET("/assets-manifest.json", serveFile("asserts-manifest.json", "application/json")) ui.GET("/index.html", serveFile("index.html", "text/html", noop))
ui.GET("/manifest.json", serveFile("manifest.json", "application/json", noop))
ui.GET("/asset-manifest.json", serveFile("asset-manifest.json", "application/json", noop))
ui.GET("/static/*any", gin.WrapH(http.FileServer(box))) ui.GET("/static/*any", gin.WrapH(http.FileServer(box)))
} }
func serveFile(name, contentType string) gin.HandlerFunc { func noop(s string) string {
return func(ctx *gin.Context) { return s
ctx.Header("Content-Type", contentType) }
func serveFile(name, contentType string, convert func(string) string) gin.HandlerFunc {
content, err := box.FindString(name) content, err := box.FindString(name)
if err != nil { if err != nil {
panic(err) panic(err)
} }
ctx.String(200, content) converted := convert(content)
return func(ctx *gin.Context) {
ctx.Header("Content-Type", contentType)
ctx.String(200, converted)
} }
} }

View File

@ -1,13 +1,29 @@
import {IVersion} from './types';
export interface IConfig { export interface IConfig {
url: string; url: string;
register: boolean;
version: IVersion;
} }
let config: IConfig; // eslint-disable-next-line @typescript-eslint/no-unused-vars
declare global {
export function set(c: IConfig) { interface Window {
config = c; config?: Partial<IConfig>;
}
} }
export function get(val: 'url'): string { const config: IConfig = {
return config[val]; url: 'unset',
register: false,
version: {commit: 'unknown', buildDate: 'unknown', version: 'unknown'},
...window.config,
};
export function set<Key extends keyof IConfig>(key: Key, value: IConfig[Key]): void {
config[key] = value;
}
export function get<K extends keyof IConfig>(key: K): IConfig[K] {
return config[key];
} }

View File

@ -16,9 +16,7 @@ import {ClientStore} from './client/ClientStore';
import {PluginStore} from './plugin/PluginStore'; import {PluginStore} from './plugin/PluginStore';
import {registerReactions} from './reactions'; import {registerReactions} from './reactions';
const defaultDevConfig = { const devUrl = 'http://localhost:3000/';
url: 'http://localhost:3000/',
};
const {port, hostname, protocol, pathname} = window.location; const {port, hostname, protocol, pathname} = window.location;
const slashes = protocol.concat('//'); const slashes = protocol.concat('//');
@ -26,16 +24,7 @@ const path = pathname.endsWith('/') ? pathname : pathname.substring(0, pathname.
const url = slashes.concat(port ? hostname.concat(':', port) : hostname) + path; const url = slashes.concat(port ? hostname.concat(':', port) : hostname) + path;
const urlWithSlash = url.endsWith('/') ? url : url.concat('/'); const urlWithSlash = url.endsWith('/') ? url : url.concat('/');
const defaultProdConfig = { const prodUrl = urlWithSlash;
url: urlWithSlash,
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
declare global {
interface Window {
config: config.IConfig;
}
}
const initStores = (): StoreMapping => { const initStores = (): StoreMapping => {
const snackManager = new SnackManager(); const snackManager = new SnackManager();
@ -62,9 +51,10 @@ const initStores = (): StoreMapping => {
(function clientJS() { (function clientJS() {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
config.set(window.config || defaultProdConfig); config.set('url', prodUrl);
} else { } else {
config.set(window.config || defaultDevConfig); config.set('url', devUrl);
config.set('register', true);
} }
const stores = initStores(); const stores = initStores();
initAxios(stores.currentUser, stores.snackManager.snack); initAxios(stores.currentUser, stores.snackManager.snack);

View File

@ -1,6 +1,5 @@
import {createMuiTheme, MuiThemeProvider, Theme, WithStyles, withStyles} from '@material-ui/core'; import {createMuiTheme, MuiThemeProvider, Theme, WithStyles, withStyles} from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import axios, {AxiosResponse} from 'axios';
import * as React from 'react'; import * as React from 'react';
import {HashRouter, Redirect, Route, Switch} from 'react-router-dom'; import {HashRouter, Redirect, Route, Switch} from 'react-router-dom';
import Header from './Header'; import Header from './Header';
@ -21,7 +20,6 @@ import {observer} from 'mobx-react';
import {observable} from 'mobx'; import {observable} from 'mobx';
import {inject, Stores} from '../inject'; import {inject, Stores} from '../inject';
import {ConnectionErrorBanner} from '../common/ConnectionErrorBanner'; import {ConnectionErrorBanner} from '../common/ConnectionErrorBanner';
import {IVersion} from '../types';
const styles = (theme: Theme) => ({ const styles = (theme: Theme) => ({
content: { content: {
@ -57,31 +55,18 @@ const isThemeKey = (value: string | null): value is ThemeKey =>
class Layout extends React.Component< class Layout extends React.Component<
WithStyles<'content'> & Stores<'currentUser' | 'snackManager'> WithStyles<'content'> & Stores<'currentUser' | 'snackManager'>
> { > {
private static defaultVersion = '0.0.0';
@observable @observable
private currentTheme: ThemeKey = 'dark'; private currentTheme: ThemeKey = 'dark';
@observable @observable
private showSettings = false; private showSettings = false;
@observable @observable
private version = Layout.defaultVersion;
@observable
private navOpen = false; private navOpen = false;
@observable
private showRegister = true; //TODO https://github.com/gotify/server/pull/394#discussion_r650559205
private setNavOpen(open: boolean) { private setNavOpen(open: boolean) {
this.navOpen = open; this.navOpen = open;
} }
public componentDidMount() { public componentDidMount() {
this.registration = true; //TODO https://github.com/gotify/server/pull/394#discussion_r650559205
if (this.version === Layout.defaultVersion) {
axios.get(config.get('url') + 'version').then((resp: AxiosResponse<IVersion>) => {
this.version = resp.data.version;
});
}
const localStorageTheme = window.localStorage.getItem(localStorageThemeKey); const localStorageTheme = window.localStorage.getItem(localStorageThemeKey);
if (isThemeKey(localStorageTheme)) { if (isThemeKey(localStorageTheme)) {
this.currentTheme = localStorageTheme; this.currentTheme = localStorageTheme;
@ -91,7 +76,7 @@ class Layout extends React.Component<
} }
public render() { public render() {
const {version, showSettings, currentTheme, showRegister} = this; const {showSettings, currentTheme} = this;
const { const {
classes, classes,
currentUser: { currentUser: {
@ -104,8 +89,8 @@ class Layout extends React.Component<
}, },
} = this.props; } = this.props;
const theme = themeMap[currentTheme]; const theme = themeMap[currentTheme];
const loginRoute = () => const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
loggedIn ? <Redirect to="/" /> : <Login showRegister={showRegister} />; const {version} = config.get('version');
return ( return (
<MuiThemeProvider theme={theme}> <MuiThemeProvider theme={theme}>
<HashRouter> <HashRouter>

View File

@ -7,14 +7,11 @@ import DefaultPage from '../common/DefaultPage';
import {observable} from 'mobx'; import {observable} from 'mobx';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import {inject, Stores} from '../inject'; import {inject, Stores} from '../inject';
import * as config from '../config';
import RegistrationDialog from './Register'; import RegistrationDialog from './Register';
type Props = Stores<'currentUser'> & {
showRegister: boolean;
};
@observer @observer
class Login extends Component<Props> { class Login extends Component<Stores<'currentUser'>> {
@observable @observable
private username = ''; private username = '';
@observable @observable
@ -75,7 +72,7 @@ class Login extends Component<Props> {
}; };
private registerButton = () => { private registerButton = () => {
if (this.props.showRegister) if (config.get('register'))
return ( return (
<Button <Button
id="register" id="register"