Skip to content

Commit

Permalink
Setting to disable signups
Browse files Browse the repository at this point in the history
  • Loading branch information
previnder committed Nov 10, 2024
1 parent 21089da commit f466eef
Show file tree
Hide file tree
Showing 17 changed files with 547 additions and 258 deletions.
95 changes: 95 additions & 0 deletions core/sitesettings/sitesettings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package sitesettings

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"sync"
)

var cache = &ssCache{}

// SiteSettings are distinct from config.Config. SiteSettings are those settings
// that can be changed on the fly (while everything is running) from the admin
// dashboard.
type SiteSettings struct {
SignupsDisabled bool `json:"signupsDisabled"`

// note: ssCache.store() and ssCache.get() uses shallow-copy on this struct.
// So those lines of code need updating if pointer fields are added to this
// struct.
}

// Save persists s to the database.
func (s *SiteSettings) Save(ctx context.Context, db *sql.DB) error {
defer cache.bust()

bytes, err := json.Marshal(s)
if err != nil {
return err
}

_, err = db.ExecContext(ctx, "INSERT INTO application_data (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?", "site_settings", string(bytes), string(bytes))
return err
}

type ssCache struct {
mu sync.Mutex // guards following
settings *SiteSettings // cache value
}

func (c *ssCache) get() *SiteSettings {
c.mu.Lock()
defer c.mu.Unlock()
if c.settings != nil {
cp := &SiteSettings{}
*cp = *c.settings // shallow copy
return cp
}
return nil
}

func (c *ssCache) store(s *SiteSettings) {
c.mu.Lock()
cp := &SiteSettings{}
*cp = *s // shallow copy
c.settings = cp
c.mu.Unlock()
}

func (c *ssCache) bust() {
c.mu.Lock()
c.settings = nil
c.mu.Unlock()
}

// GetSiteSettings retrieves the site settings (of the admin dashboard) from the
// database. If the data is not found in the database (as the case may be if
// they were never altered), then the default settings object is returned.
func GetSiteSettings(ctx context.Context, db *sql.DB) (*SiteSettings, error) {
if settings := cache.get(); settings != nil {
return settings, nil
}

var jsonText string
err := db.QueryRowContext(ctx, "SELECT `value` FROM application_data WHERE `key` = ?", "site_settings").Scan(&jsonText)
if err != nil {
if err != sql.ErrNoRows {
return nil, fmt.Errorf("error reading site_settings from db: %w", err)
}
}

settings := &SiteSettings{}
if jsonText == "" {
// Do nothhing. No row was found in the application_data table. Return
// the deafult SiteSettings object.
} else {
if err = json.Unmarshal([]byte(jsonText), settings); err != nil {
return nil, err
}
}

cache.store(settings)
return settings, nil
}
24 changes: 24 additions & 0 deletions server/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

"github.com/discuitnet/discuit/core"
"github.com/discuitnet/discuit/core/sitesettings"
"github.com/discuitnet/discuit/internal/httperr"
)

Expand Down Expand Up @@ -155,3 +156,26 @@ func (s *Server) getUsers(w *responseWriter, r *request) error {

return w.writeJSON(res)
}

func (s *Server) handleSiteSettings(w *responseWriter, r *request) error {
_, err := getLoggedInAdmin(s.db, r)
if err != nil {
return err
}

settings, err := sitesettings.GetSiteSettings(r.ctx, s.db)
if err != nil {
return err
}

if r.req.Method == "PUT" {
if err = r.unmarshalJSONBody(settings); err != nil {
return err
}
if err = settings.Save(r.ctx, s.db); err != nil {
return err
}
}

return w.writeJSON(settings)
}
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ func New(db *sql.DB, conf *config.Config) (*Server, error) {
r.Handle("/api/_link_info", s.withHandler(s.getLinkInfo)).Methods("GET")

r.Handle("/api/analytics", s.withHandler(s.handleAnalytics)).Methods("POST")
r.Handle("/api/site_settings", s.withHandler(s.handleSiteSettings)).Methods("GET", "PUT")

r.NotFoundHandler = http.HandlerFunc(s.apiNotFoundHandler)
r.MethodNotAllowedHandler = http.HandlerFunc(s.apiMethodNotAllowedHandler)
Expand Down
31 changes: 23 additions & 8 deletions server/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/SherClockHolmes/webpush-go"
"github.com/discuitnet/discuit/core"
"github.com/discuitnet/discuit/core/sitesettings"
"github.com/discuitnet/discuit/internal/hcaptcha"
"github.com/discuitnet/discuit/internal/httperr"
"github.com/discuitnet/discuit/internal/httputil"
Expand Down Expand Up @@ -110,14 +111,15 @@ func (s *Server) deleteUser(w *responseWriter, r *request) error {
func (s *Server) initial(w *responseWriter, r *request) error {
var err error
response := struct {
ReportReasons []core.ReportReason `json:"reportReasons"`
User *core.User `json:"user"`
Lists []*core.List `json:"lists"`
Communities []*core.Community `json:"communities"`
NoUsers int `json:"noUsers"`
BannedFrom []uid.ID `json:"bannedFrom"`
VAPIDPublicKey string `json:"vapidPublicKey"`
Mutes struct {
SignupsDisabled bool `json:"signupsDisabled"`
ReportReasons []core.ReportReason `json:"reportReasons"`
User *core.User `json:"user"`
Lists []*core.List `json:"lists"`
Communities []*core.Community `json:"communities"`
NoUsers int `json:"noUsers"`
BannedFrom []uid.ID `json:"bannedFrom"`
VAPIDPublicKey string `json:"vapidPublicKey"`
Mutes struct {
CommunityMutes []*core.Mute `json:"communityMutes"`
UserMutes []*core.Mute `json:"userMutes"`
} `json:"mutes"`
Expand All @@ -129,6 +131,12 @@ func (s *Server) initial(w *responseWriter, r *request) error {
response.Mutes.CommunityMutes = []*core.Mute{}
response.Mutes.UserMutes = []*core.Mute{}

siteSettings, err := sitesettings.GetSiteSettings(r.ctx, s.db)
if err != nil {
return err
}
response.SignupsDisabled = siteSettings.SignupsDisabled

if r.loggedIn {
if response.User, err = core.GetUser(r.ctx, s.db, *r.viewer, r.viewer); err != nil {
if httperr.IsNotFound(err) {
Expand Down Expand Up @@ -238,6 +246,13 @@ func (s *Server) signup(w *responseWriter, r *request) error {
return httperr.NewBadRequest("already_logged_in", "You are already logged in")
}

// Verify that signups are not disabled.
if settings, err := sitesettings.GetSiteSettings(r.ctx, s.db); err != nil {
return err
} else if settings.SignupsDisabled {
return httperr.NewForbidden("signups-disabled", "Creating new accounts is disabled.")
}

values, err := r.unmarshalJSONBodyToStringsMap(true)
if err != nil {
return err
Expand Down
19 changes: 2 additions & 17 deletions ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,9 @@ import Terms from './pages/Terms';
import User from './pages/User';
import PushNotifications from './PushNotifications';
import {
bannedFromUpdated,
createCommunityModalOpened,
initialValuesAdded,
listsAdded,
initialFieldsSet,
loginModalOpened,
mutesAdded,
noUsersUpdated,
reportReasonsUpdated,
sidebarCommunitiesUpdated,
signupModalOpened,
snackAlert,
toggleSidebarOpen,
Expand Down Expand Up @@ -111,16 +105,7 @@ const App = () => {
// Cookies.
window.localStorage.setItem('csrftoken', res.headers.get('Csrf-Token'));

if (initial.user) {
dispatch(userLoggedIn(initial.user));
}
dispatch(sidebarCommunitiesUpdated(initial.communities));
dispatch(reportReasonsUpdated(initial.reportReasons));
dispatch(noUsersUpdated(initial.noUsers));
dispatch(bannedFromUpdated(initial.bannedFrom || []));
dispatch(initialValuesAdded(initial)); // miscellaneous data
dispatch(mutesAdded(initial.mutes));
dispatch(listsAdded(initial.lists));
dispatch(initialFieldsSet(initial));
} catch (err) {
console.error(err);
dispatch(snackAlert('Something went wrong.'));
Expand Down
22 changes: 18 additions & 4 deletions ui/src/components/Signup.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import clsx from 'clsx';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useRef, useState } from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
import { Helmet } from 'react-helmet-async';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { usernameMaxLength } from '../config';
import { APIError, mfetch, validEmail } from '../helper';
import { useDelayedEffect, useInputUsername } from '../hooks';
Expand All @@ -25,6 +26,8 @@ const errors = [
const Signup = ({ open, onClose }) => {
const dispatch = useDispatch();

const signupsDisabled = useSelector((state) => state.main.signupsDisabled);

const [username, handleUsernameChange] = useInputUsername(usernameMaxLength);
const [usernameError, setUsernameError] = useState(null);
const checkUsernameExists = useCallback(async () => {
Expand Down Expand Up @@ -147,12 +150,15 @@ const Signup = ({ open, onClose }) => {
<style>{`.grecaptcha-badge { visibility: hidden; }`}</style>
</Helmet>
<Modal open={open} onClose={onClose} noOuterClickClose={false}>
<div className="modal-card modal-signup">
<div className={clsx('modal-card modal-signup', signupsDisabled && 'is-disabled')}>
<div className="modal-card-head">
<div className="modal-card-title">Signup</div>
<ButtonClose onClick={onClose} />
</div>
<Form className="modal-card-content" onSubmit={handleSubmit}>
{signupsDisabled && (
<div className="modal-signup-disabled">{`We have temporarily disabled creating new accounts. Please check back again later.`}</div>
)}
<FormField
className="is-username"
label="Username"
Expand All @@ -166,20 +172,27 @@ const Signup = ({ open, onClose }) => {
onBlur={() => checkUsernameExists()}
autoFocus
autoComplete="username"
disabled={signupsDisabled}
/>
</FormField>
<FormField
label="Email (optional)"
description="Without an email address, there's no way to recover your account if you lose your password."
error={emailError}
>
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={signupsDisabled}
/>
</FormField>
<FormField label="Password" error={passwordError}>
<InputPassword
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="new-password"
disabled={signupsDisabled}
/>
</FormField>
<FormField label="Repeat password" error={repeatPasswordError}>
Expand All @@ -189,6 +202,7 @@ const Signup = ({ open, onClose }) => {
setRepeatPassword(e.target.value);
}}
autoComplete="new-password"
disabled={signupsDisabled}
/>
</FormField>
{CAPTCHA_ENABLED && (
Expand Down Expand Up @@ -228,7 +242,7 @@ const Signup = ({ open, onClose }) => {
</FormField>
<FormField className="is-submit">
<input type="submit" className="button button-main" value="Signup" />
<button className="button-link" onClick={handleOnLogin}>
<button className="button-link" onClick={handleOnLogin} disabled={signupsDisabled}>
Already have an account? Login
</button>
</FormField>
Expand Down
3 changes: 2 additions & 1 deletion ui/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,13 @@ export function useQuery() {
return new URLSearchParams(useLocation().search);
}

export function useIsChanged(deps = []) {
export function useIsChanged(deps: unknown[] = []): [boolean, () => void] {
const [c, setC] = useState(0);
useEffect(() => {
if (c < 3) setC((c) => c + 1);
}, deps);
const resetChanged = () => setC(1);
console.log('c: ', c);
return [c > 1, resetChanged];
}

Expand Down
10 changes: 0 additions & 10 deletions ui/src/pages/AdminDashboard/Home.tsx

This file was deleted.

Loading

0 comments on commit f466eef

Please sign in to comment.