Skip to content

Commit

Permalink
Inject register & version information into index.html
Browse files Browse the repository at this point in the history
The registration form will always be shown inside the dev mode,
because there is no api that transmits if registration is enabled.
  • Loading branch information
jmattheis committed Aug 4, 2021
1 parent c172590 commit 36eb8d8
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 56 deletions.
2 changes: 1 addition & 1 deletion router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions ui/.eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions ui/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
41 changes: 30 additions & 11 deletions ui/serve.go
Original file line number Diff line number Diff line change
@@ -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 {
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)
}
converted := convert(content)
return func(ctx *gin.Context) {
ctx.Header("Content-Type", contentType)
content, err := box.FindString(name)
if err != nil {
panic(err)
}
ctx.String(200, content)
ctx.String(200, converted)
}
}
26 changes: 21 additions & 5 deletions ui/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import {IVersion} from './types';

export interface IConfig {
url: string;
register: boolean;
version: IVersion;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
declare global {
interface Window {
config?: Partial<IConfig>;
}
}

let config: IConfig;
const config: IConfig = {
url: 'unset',
register: false,
version: {commit: 'unknown', buildDate: 'unknown', version: 'unknown'},
...window.config,
};

export function set(c: IConfig) {
config = c;
export function set<Key extends keyof IConfig>(key: Key, value: IConfig[Key]): void {
config[key] = value;
}

export function get(val: 'url'): string {
return config[val];
export function get<K extends keyof IConfig>(key: K): IConfig[K] {
return config[key];
}
20 changes: 5 additions & 15 deletions ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,15 @@ 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('//');
const path = pathname.endsWith('/') ? pathname : pathname.substring(0, pathname.lastIndexOf('/'));
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();
Expand All @@ -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);
Expand Down
21 changes: 3 additions & 18 deletions ui/src/layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: {
Expand Down Expand Up @@ -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;
Expand All @@ -91,7 +76,7 @@ class Layout extends React.Component<
}

public render() {
const {version, showSettings, currentTheme, showRegister} = this;
const {showSettings, currentTheme} = this;
const {
classes,
currentUser: {
Expand All @@ -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>
Expand Down
9 changes: 3 additions & 6 deletions ui/src/user/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -75,7 +72,7 @@ class Login extends Component<Props> {
};

private registerButton = () => {
if (this.props.showRegister)
if (config.get('register'))
return (
<Button
id="register"
Expand Down

0 comments on commit 36eb8d8

Please sign in to comment.