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.OnUserAdded(pluginManager.InitializeForUserID)
ui.Register(g)
ui.Register(g, *vInfo, conf.Registration)
g.GET("/health", healthHandler.Health)
g.GET("/swagger", docs.Serve)

View File

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

View File

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

View File

@ -1,32 +1,51 @@
package ui
import (
"encoding/json"
"net/http"
"strings"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/gobuffalo/packr/v2"
"github.com/gotify/server/v2/model"
)
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.
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.GET("/", serveFile("index.html", "text/html"))
ui.GET("/index.html", serveFile("index.html", "text/html"))
ui.GET("/manifest.json", serveFile("manifest.json", "application/json"))
ui.GET("/assets-manifest.json", serveFile("asserts-manifest.json", "application/json"))
ui.GET("/", serveFile("index.html", "text/html", func(content string) string {
return strings.Replace(content, "%CONFIG%", string(uiConfigBytes), 1)
}))
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)))
}
func serveFile(name, contentType string) gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Header("Content-Type", contentType)
func noop(s string) string {
return s
}
func serveFile(name, contentType string, convert func(string) string) gin.HandlerFunc {
content, err := box.FindString(name)
if err != nil {
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 {
url: string;
register: boolean;
version: IVersion;
}
let config: IConfig;
export function set(c: IConfig) {
config = c;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
declare global {
interface Window {
config?: Partial<IConfig>;
}
}
export function get(val: 'url'): string {
return config[val];
const config: IConfig = {
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 {registerReactions} from './reactions';
const defaultDevConfig = {
url: 'http://localhost:3000/',
};
const devUrl = 'http://localhost:3000/';
const {port, hostname, protocol, pathname} = window.location;
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 urlWithSlash = url.endsWith('/') ? url : url.concat('/');
const defaultProdConfig = {
url: urlWithSlash,
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
declare global {
interface Window {
config: config.IConfig;
}
}
const prodUrl = urlWithSlash;
const initStores = (): StoreMapping => {
const snackManager = new SnackManager();
@ -62,9 +51,10 @@ const initStores = (): StoreMapping => {
(function clientJS() {
if (process.env.NODE_ENV === 'production') {
config.set(window.config || defaultProdConfig);
config.set('url', prodUrl);
} else {
config.set(window.config || defaultDevConfig);
config.set('url', devUrl);
config.set('register', true);
}
const stores = initStores();
initAxios(stores.currentUser, stores.snackManager.snack);

View File

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

View File

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