stuff
parent
d982ce13f5
commit
b5e2c83fba
|
@ -53,9 +53,11 @@ var (
|
||||||
errHTTPBadRequestMatrixMessageInvalid = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway"}
|
errHTTPBadRequestMatrixMessageInvalid = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway"}
|
||||||
errHTTPBadRequestMatrixPushkeyBaseURLMismatch = &errHTTP{40020, http.StatusBadRequest, "invalid request: push key must be prefixed with base URL", "https://ntfy.sh/docs/publish/#matrix-gateway"}
|
errHTTPBadRequestMatrixPushkeyBaseURLMismatch = &errHTTP{40020, http.StatusBadRequest, "invalid request: push key must be prefixed with base URL", "https://ntfy.sh/docs/publish/#matrix-gateway"}
|
||||||
errHTTPBadRequestIconURLInvalid = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons"}
|
errHTTPBadRequestIconURLInvalid = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons"}
|
||||||
|
errHTTPBadRequestSignupNotEnabled = &errHTTP{40022, http.StatusBadRequest, "invalid request: signup not enabled", "https://ntfy.sh/docs/config"}
|
||||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
|
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
|
||||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
|
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
|
||||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
|
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
|
||||||
|
errHTTPConflictUserExists = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", ""}
|
||||||
errHTTPEntityTooLargeAttachmentTooLarge = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"}
|
errHTTPEntityTooLargeAttachmentTooLarge = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"}
|
||||||
errHTTPEntityTooLargeMatrixRequestTooLarge = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", ""}
|
errHTTPEntityTooLargeMatrixRequestTooLarge = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", ""}
|
||||||
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
|
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
|
||||||
|
|
|
@ -47,6 +47,7 @@ import (
|
||||||
purge accounts that were not logged into in X
|
purge accounts that were not logged into in X
|
||||||
sync subscription display name
|
sync subscription display name
|
||||||
store users
|
store users
|
||||||
|
signup: check unique user
|
||||||
Pages:
|
Pages:
|
||||||
- Home
|
- Home
|
||||||
- Password reset
|
- Password reset
|
||||||
|
@ -1307,7 +1308,17 @@ func (s *Server) sendDelayedMessages() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
v := s.visitorFromID(fmt.Sprintf("ip:%s", m.Sender.String()), m.Sender, nil) // FIXME: This is wrong wrong wrong
|
var v *visitor
|
||||||
|
if m.User != "" {
|
||||||
|
user, err := s.auth.User(m.User)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("%s Error sending delayed message: %s", logMessagePrefix(v, m), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v = s.visitorFromUser(user, m.Sender)
|
||||||
|
} else {
|
||||||
|
v = s.visitorFromIP(m.Sender)
|
||||||
|
}
|
||||||
if err := s.sendDelayedMessage(v, m); err != nil {
|
if err := s.sendDelayedMessage(v, m); err != nil {
|
||||||
log.Warn("%s Error sending delayed message: %s", logMessagePrefix(v, m), err.Error())
|
log.Warn("%s Error sending delayed message: %s", logMessagePrefix(v, m), err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1462,18 +1473,18 @@ func (s *Server) autorizeTopic(next handleFunc, perm auth.Permission) handleFunc
|
||||||
// visitor creates or retrieves a rate.Limiter for the given visitor.
|
// visitor creates or retrieves a rate.Limiter for the given visitor.
|
||||||
// Note that this function will always return a visitor, even if an error occurs.
|
// Note that this function will always return a visitor, even if an error occurs.
|
||||||
func (s *Server) visitor(r *http.Request) (v *visitor, err error) {
|
func (s *Server) visitor(r *http.Request) (v *visitor, err error) {
|
||||||
ip := s.extractIPAddress(r)
|
ip := extractIPAddress(r, s.config.BehindProxy)
|
||||||
visitorID := fmt.Sprintf("ip:%s", ip.String())
|
|
||||||
var user *auth.User // may stay nil if no auth header!
|
var user *auth.User // may stay nil if no auth header!
|
||||||
if user, err = s.authenticate(r); err != nil {
|
if user, err = s.authenticate(r); err != nil {
|
||||||
log.Debug("authentication failed: %s", err.Error())
|
log.Debug("authentication failed: %s", err.Error())
|
||||||
err = errHTTPUnauthorized // Always return visitor, even when error occurs!
|
err = errHTTPUnauthorized // Always return visitor, even when error occurs!
|
||||||
}
|
}
|
||||||
if user != nil {
|
if user != nil {
|
||||||
visitorID = fmt.Sprintf("user:%s", user.Name)
|
v = s.visitorFromUser(user, ip)
|
||||||
|
} else {
|
||||||
|
v = s.visitorFromIP(ip)
|
||||||
}
|
}
|
||||||
v = s.visitorFromID(visitorID, ip, user)
|
v.user = user // Update user -- FIXME race?
|
||||||
v.user = user // Update user -- FIXME this is ugly, do "newVisitorFromUser" instead
|
|
||||||
return v, err // Always return visitor, even when error occurs!
|
return v, err // Always return visitor, even when error occurs!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1526,30 +1537,10 @@ func (s *Server) visitorFromID(visitorID string, ip netip.Addr, user *auth.User)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) extractIPAddress(r *http.Request) netip.Addr {
|
func (s *Server) visitorFromIP(ip netip.Addr) *visitor {
|
||||||
remoteAddr := r.RemoteAddr
|
return s.visitorFromID(fmt.Sprintf("ip:%s", ip.String()), ip, nil)
|
||||||
addrPort, err := netip.ParseAddrPort(remoteAddr)
|
}
|
||||||
ip := addrPort.Addr()
|
|
||||||
if err != nil {
|
func (s *Server) visitorFromUser(user *auth.User, ip netip.Addr) *visitor {
|
||||||
// This should not happen in real life; only in tests. So, using falling back to 0.0.0.0 if address unspecified
|
return s.visitorFromID(fmt.Sprintf("user:%s", user.Name), ip, user)
|
||||||
ip, err = netip.ParseAddr(remoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
ip = netip.IPv4Unspecified()
|
|
||||||
log.Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created %s", remoteAddr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.config.BehindProxy && strings.TrimSpace(r.Header.Get("X-Forwarded-For")) != "" {
|
|
||||||
// X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy,
|
|
||||||
// only the right-most address can be trusted (as this is the one added by our proxy server).
|
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details.
|
|
||||||
ips := util.SplitNoEmpty(r.Header.Get("X-Forwarded-For"), ",")
|
|
||||||
realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr)))
|
|
||||||
if err != nil {
|
|
||||||
log.Error("invalid IP address %s received in X-Forwarded-For header: %s", ip, err.Error())
|
|
||||||
// Fall back to regular remote address if X-Forwarded-For is damaged
|
|
||||||
} else {
|
|
||||||
ip = realIP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ip
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
|
||||||
signupAllowed := s.config.EnableSignup
|
signupAllowed := s.config.EnableSignup
|
||||||
admin := v.user != nil && v.user.Role == auth.RoleAdmin
|
admin := v.user != nil && v.user.Role == auth.RoleAdmin
|
||||||
if !signupAllowed && !admin {
|
if !signupAllowed && !admin {
|
||||||
return errHTTPUnauthorized
|
return errHTTPBadRequestSignupNotEnabled
|
||||||
}
|
}
|
||||||
body, err := util.Peek(r.Body, 4096) // FIXME
|
body, err := util.Peek(r.Body, 4096) // FIXME
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -23,6 +23,9 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
|
||||||
if err := json.NewDecoder(body).Decode(&newAccount); err != nil {
|
if err := json.NewDecoder(body).Decode(&newAccount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if existingUser, _ := s.auth.User(newAccount.Username); existingUser != nil {
|
||||||
|
return errHTTPConflictUserExists
|
||||||
|
}
|
||||||
if err := s.auth.AddUser(newAccount.Username, newAccount.Password, auth.RoleUser); err != nil { // TODO this should return a User
|
if err := s.auth.AddUser(newAccount.Username, newAccount.Password, auth.RoleUser); err != nil { // TODO this should return a User
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,10 @@ package server
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
|
"heckel.io/ntfy/log"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
@ -89,3 +91,31 @@ func renderHTTPRequest(r *http.Request) string {
|
||||||
r.Body = body // Important: Reset body, so it can be re-read
|
r.Body = body // Important: Reset body, so it can be re-read
|
||||||
return strings.TrimSpace(lines)
|
return strings.TrimSpace(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr {
|
||||||
|
remoteAddr := r.RemoteAddr
|
||||||
|
addrPort, err := netip.ParseAddrPort(remoteAddr)
|
||||||
|
ip := addrPort.Addr()
|
||||||
|
if err != nil {
|
||||||
|
// This should not happen in real life; only in tests. So, using falling back to 0.0.0.0 if address unspecified
|
||||||
|
ip, err = netip.ParseAddr(remoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
ip = netip.IPv4Unspecified()
|
||||||
|
log.Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created %s", remoteAddr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if behindProxy && strings.TrimSpace(r.Header.Get("X-Forwarded-For")) != "" {
|
||||||
|
// X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy,
|
||||||
|
// only the right-most address can be trusted (as this is the one added by our proxy server).
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details.
|
||||||
|
ips := util.SplitNoEmpty(r.Header.Get("X-Forwarded-For"), ",")
|
||||||
|
realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr)))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("invalid IP address %s received in X-Forwarded-For header: %s", ip, err.Error())
|
||||||
|
// Fall back to regular remote address if X-Forwarded-For is damaged
|
||||||
|
} else {
|
||||||
|
ip = realIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
|
@ -149,18 +149,6 @@ class Api {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async userStats(baseUrl) {
|
|
||||||
const url = userStatsUrl(baseUrl);
|
|
||||||
console.log(`[Api] Fetching user stats ${url}`);
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(`Unexpected server response ${response.status}`);
|
|
||||||
}
|
|
||||||
const stats = await response.json();
|
|
||||||
console.log(`[Api] Stats`, stats);
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createAccount(baseUrl, username, password) {
|
async createAccount(baseUrl, username, password) {
|
||||||
const url = accountUrl(baseUrl);
|
const url = accountUrl(baseUrl);
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
|
|
|
@ -18,7 +18,6 @@ export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, top
|
||||||
export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
|
export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
|
||||||
export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
|
export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
|
||||||
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
|
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
|
||||||
export const userStatsUrl = (baseUrl) => `${baseUrl}/user/stats`;
|
|
||||||
export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`;
|
export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`;
|
||||||
export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`;
|
export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`;
|
||||||
export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`;
|
export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`;
|
||||||
|
|
|
@ -264,11 +264,10 @@ const ProfileIcon = (props) => {
|
||||||
session.reset();
|
session.reset();
|
||||||
window.location.href = routes.app;
|
window.location.href = routes.app;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{session.exists() &&
|
{session.exists() &&
|
||||||
<IconButton color="inherit" size="large" edge="end" onClick={handleClick} sx={{marginRight: 0}} aria-label={t("xxxxxxx")}>
|
<IconButton color="inherit" size="large" edge="end" onClick={handleClick} aria-label={t("xxxxxxx")}>
|
||||||
<AccountCircleIcon/>
|
<AccountCircleIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,11 @@ import {useState} from "react";
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const data = new FormData(event.currentTarget);
|
const user = { username, password };
|
||||||
const user = {
|
|
||||||
username: data.get('username'),
|
|
||||||
password: data.get('password'),
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const token = await api.login(config.baseUrl, user);
|
const token = await api.login(config.baseUrl, user);
|
||||||
if (token) {
|
if (token) {
|
||||||
|
@ -61,6 +59,8 @@ const Login = () => {
|
||||||
id="username"
|
id="username"
|
||||||
label={t("Username")}
|
label={t("Username")}
|
||||||
name="username"
|
name="username"
|
||||||
|
value={username}
|
||||||
|
onChange={ev => setUsername(ev.target.value.trim())}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -71,12 +71,15 @@ const Login = () => {
|
||||||
label={t("Password")}
|
label={t("Password")}
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
|
value={password}
|
||||||
|
onChange={ev => setPassword(ev.target.value.trim())}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
disabled={username === "" || password === ""}
|
||||||
sx={{mt: 2, mb: 2}}
|
sx={{mt: 2, mb: 2}}
|
||||||
>
|
>
|
||||||
{t("Sign in")}
|
{t("Sign in")}
|
||||||
|
|
|
@ -9,21 +9,37 @@ import Typography from "@mui/material/Typography";
|
||||||
import {NavLink} from "react-router-dom";
|
import {NavLink} from "react-router-dom";
|
||||||
import AvatarBox from "./AvatarBox";
|
import AvatarBox from "./AvatarBox";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {useState} from "react";
|
||||||
|
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
||||||
|
|
||||||
const Signup = () => {
|
const Signup = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirm, setConfirm] = useState("");
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const data = new FormData(event.currentTarget);
|
const user = { username, password };
|
||||||
const user = {
|
try {
|
||||||
username: data.get('username'),
|
await api.createAccount(config.baseUrl, user.username, user.password);
|
||||||
password: data.get('password')
|
const token = await api.login(config.baseUrl, user);
|
||||||
};
|
if (token) {
|
||||||
await api.createAccount(config.baseUrl, user.username, user.password);
|
console.log(`[Signup] User signup for user ${user.username} successful, token is ${token}`);
|
||||||
const token = await api.login(config.baseUrl, user);
|
session.store(user.username, token);
|
||||||
console.log(`[Api] User auth for user ${user.username} successful, token is ${token}`);
|
window.location.href = routes.app;
|
||||||
session.store(user.username, token);
|
} else {
|
||||||
window.location.href = routes.app;
|
console.log(`[Signup] Signup for user ${user.username} failed, access denied`);
|
||||||
|
setError(t("Login failed: Invalid username or password"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[Signup] Signup for user ${user.username} failed`, e);
|
||||||
|
if (e && e.message) {
|
||||||
|
setError(e.message);
|
||||||
|
} else {
|
||||||
|
setError(t("Unknown error. Check logs for details."))
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (!config.enableSignup) {
|
if (!config.enableSignup) {
|
||||||
return (
|
return (
|
||||||
|
@ -45,6 +61,8 @@ const Signup = () => {
|
||||||
id="username"
|
id="username"
|
||||||
label="Username"
|
label="Username"
|
||||||
name="username"
|
name="username"
|
||||||
|
value={username}
|
||||||
|
onChange={ev => setUsername(ev.target.value.trim())}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -56,6 +74,8 @@ const Signup = () => {
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
|
value={password}
|
||||||
|
onChange={ev => setPassword(ev.target.value.trim())}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
|
@ -65,15 +85,30 @@ const Signup = () => {
|
||||||
label="Confirm password"
|
label="Confirm password"
|
||||||
type="password"
|
type="password"
|
||||||
id="confirm-password"
|
id="confirm-password"
|
||||||
|
value={confirm}
|
||||||
|
onChange={ev => setConfirm(ev.target.value.trim())}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
disabled={username === "" || password === "" || password !== confirm}
|
||||||
sx={{mt: 2, mb: 2}}
|
sx={{mt: 2, mb: 2}}
|
||||||
>
|
>
|
||||||
{t("Sign up")}
|
{t("Sign up")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{error &&
|
||||||
|
<Box sx={{
|
||||||
|
mb: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<WarningAmberIcon color="error" sx={{mr: 1}}/>
|
||||||
|
<Typography sx={{color: 'error.main'}}>{error}</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
</Box>
|
</Box>
|
||||||
{config.enableLogin &&
|
{config.enableLogin &&
|
||||||
<Typography sx={{mb: 4}}>
|
<Typography sx={{mb: 4}}>
|
||||||
|
|
Loading…
Reference in New Issue