OurBigBook logoOurBigBook Docs OurBigBook logoOurBigBook.comSite Source code
web/pages/go/settings/[uid]/index.tsx
import Router from 'next/router'
import React from 'react'

import lodash from 'lodash'

import {
  allowedImageContentTypes,
  allowedImageContentTypesSimplifiedArr,
  contactUrl,
  docsAccountLockingUrl,
  profilePicturePath,
  profilePictureMaxUploadSize,
} from 'front/config'
import CustomImage from 'front/CustomImage'
import Label from 'front/Label'
import MapErrors from 'front/MapErrors'
import {
  addCommasToInteger,
  HelpIcon,
  LockIcon,
  MyHead,
  OkIcon,
  SettingsIcon,
  setupUserLocalStorage,
  useCtrlEnterSubmit
} from 'front'
import { webApi } from 'front/api'
import routes from 'front/routes'
import { CommonPropsType } from 'front/types/CommonPropsType'
import { UserType } from 'front/types/UserType'
import { displayAndUsernameText } from 'front/user'
import { formatNumberApprox } from 'ourbigbook'

const maxArticleSizeLabel = "Maximum number of articles, issues and comments (maxArticles)"
const maxArticlesLabel = "Maximum article/issue/comment size (maxArticleSize)"
const maxIssuesPerMinuteLabel = "Maximum issues/comments per minute (maxIssuesPerMinute)"
const maxIssuesPerHourLabel = "Maximum issues/comments per hour (maxIssuesPerHour)"
const title = "Account settings"

interface SettingsProps extends CommonPropsType {
  user?: UserType;
}

const Settings = ({
  user: user0,
  loggedInUser,
}: SettingsProps) => {
  const [isLoading, setLoading] = React.useState(false);
  const [errors, setErrors] = React.useState([]);
  const username = user0.username
  const [userInfo, setUserInfo] = React.useState(lodash.pick(
    user0,
    [
      'displayName',
      'emailNotifications',
      'hideArticleDates',
      'password',
    ]
  ))
  const profileImageRef = React.useRef<HTMLImageElement|null>(null)
  const updateState = (field) => (e) => {
    const state = userInfo;
    const newState = { ...state, [field]: e.target.value };
    setUserInfo(newState);
  }
  const handleSubmit = async (e) => {
    e.preventDefault()
    setLoading(true)
    const user = { ...userInfo }
    if (!user.password) {
      delete user.password
    }
    const { data, status } = await webApi.userUpdate(user0.username, user)
    setLoading(false)
    if (status === 200) {
      if (
        data.user &&
        // Possible for admin edits.
        data.user.username === loggedInUser.username
      ) {
        await setupUserLocalStorage(data.user, setErrors)
      }
      Router.push(routes.user(data.user.username))
    } else {
      setErrors(data.errors)
    }
  }
  useCtrlEnterSubmit(handleSubmit)

  // Limits.
  const [userInfoLimits, setUserInfoLimits] = React.useState(lodash.pick(
    user0,
    [
      'locked',
      'maxArticleSize',
      'maxArticles',
      'maxIssuesPerHour',
      'maxIssuesPerMinute',
    ]
  ))
  const updateStateLimits = (field) => (e) => {
    const state = userInfoLimits
    const newState = { ...state, [field]: e.target.value }
    setUserInfoLimits(newState)
  }
  const handleSubmitLimits = async (e) => {
    e.preventDefault()
    setLoading(true)
    const { data, status } = await webApi.userUpdate(user0.username, userInfoLimits)
    setLoading(false)
    if (status === 200) {
      Router.push(routes.user(data.user.username))
    } else {
      setErrors(data.errors)
    }
  }

  const emailNotificationsForArticleAnnouncementRef = React.useRef(null)
  const cantSetUserLimit = !!cant.setUserLimits(loggedInUser)
  return <>
    <MyHead title={`${title} - ${displayAndUsernameText(userInfo)}`} />
    <div className="settings-page content-not-ourbigbook">
      <h1><SettingsIcon /> {title}</h1>
      <>
        <MapErrors errors={errors} />
        <form onSubmit={handleSubmit}>
          <Label label="Username">
            <input
              type="text"
              disabled={true}
              placeholder="Username"
              value={user0.username}
              title="Cannot be currently modified"
              autoComplete="username"
              //onChange={updateState("username")}
            />
          </Label>
          <Label label="Display name">
            <input
              type="text"
              placeholder="Display name"
              value={userInfo.displayName}
              onChange={updateState("displayName")}
            />
          </Label>
          <Label label="Profile picture">
            <span
              className="profile-picture-container"
              onClick={() => {
                const input = document.createElement('input')
                input.type = 'file'
                input.onchange = e => { 
                  var file = (e.target as HTMLInputElement).files[0]
                  if (file.size > profilePictureMaxUploadSize) {
                    alert(`File too large: ${addCommasToInteger(file.size)} bytes. Maximum allowed size: ${formatNumberApprox(profilePictureMaxUploadSize)}B`)
                  } else if (!allowedImageContentTypes.has(file.type)) {
                    alert(`File type not allowed: ${file.type.split('/')[1]}. Allowed types: ${allowedImageContentTypesSimplifiedArr.join(', ')}`)
                  } else {
                    var reader = new FileReader()
                    reader.readAsDataURL(file)
                    reader.onload = async (readerEvent) => {
                      const { data, status } = await webApi.userUpdateProfilePicture(
                        user0.username,
                        readerEvent.target.result,
                      )
                      if (status === 200) {
                        profileImageRef.current.src = `${profilePicturePath}/${user0.id}`
                      } else {
                        let msg = `Upload failed with status: ${status}`
                        if (data.errors) {
                          msg += `. Error message: ${data.errors[0]}`
                        }
                        alert(msg)
                      }
                    }
                  }
                }
                input.click()
              }}
            >
              <CustomImage
                className="profile-picture"
                imgRef={profileImageRef}
                src={user0.effectiveImage}
              />
              <span className="profile-picture-caption">Click to update</span>
            </span>
          </Label>
          <Label label="Email">
            <input
              type="email"
              placeholder="Email"
              value={user0.email}
              onChange={updateState("email")}
              // https://github.com/ourbigbook/ourbigbook/issues/268
              disabled={true}
              title="Cannot be currently modified"
            />
          </Label>
          <Label label="Password">
            <input
              type="password"
              placeholder="New Password"
              value={userInfoLimits.password}
              onChange={updateState("password")}
              autoComplete="new-password"
            />
          </Label>
          <Label label="Email notifications" inline={true}>
            <input
              type="checkbox"
              defaultChecked={userInfoLimits.emailNotifications}
              onChange={() => {
                setUserInfo((state) => {
                  const newState = !state.emailNotifications
                  const emailNotificationsForArticleAnnouncementElem = emailNotificationsForArticleAnnouncementRef.current
                  if (emailNotificationsForArticleAnnouncementElem) {
                    emailNotificationsForArticleAnnouncementElem.disabled = !newState
                  }
                  return {
                    ...state,
                    emailNotifications: newState
                  }}
                )
              }}
            />
          </Label>
          <Label label="Email notifications for article announcements" inline={true}>
            <input
              type="checkbox"
              defaultChecked={userInfo.emailNotificationsForArticleAnnouncement}
              ref={emailNotificationsForArticleAnnouncementRef}
              onChange={() => setUserInfo((state) => { return {
                ...state,
                emailNotificationsForArticleAnnouncement: !state.emailNotificationsForArticleAnnouncement
              }})}
            />
          </Label>
          <Label label="Hide article dates" inline={true}>
            <input
              type="checkbox"
              defaultChecked={userInfo.hideArticleDates}
              title="Hardcode the created and updated date of every edited or created article to January 1st 1970. That fake date is stored in the database instead of the real dates which are lost forever. Sequential IDs are still stored in the database, which would allos for a subpoena to infer dates from nearby ID ranges."
              onChange={() => setUserInfo((state) => { return {
                ...state,
                hideArticleDates: !state.hideArticleDates
              }})}
            />
          </Label>
          <button
            className="btn"
            type="submit"
            disabled={isLoading}
          >
            <OkIcon /> Update settings
          </button>
        </form>
        <h2><LockIcon /> Limits</h2>
        <p>You must <a href={contactUrl}><b>ask an admin</b></a> to change the following limits for you:</p>
        <form onSubmit={handleSubmitLimits}>
          <Label label={maxArticlesLabel}>
            <input
              disabled={cantSetUserLimit}
              type="number"
              value={userInfoLimits.maxArticleSize}
              onChange={updateStateLimits("maxArticleSize")}
            />
          </Label>
          <Label label={maxArticleSizeLabel}>
            <input
              disabled={cantSetUserLimit}
              type="number"
              value={userInfoLimits.maxArticles}
              onChange={updateStateLimits("maxArticles")}
            />
          </Label>
          <Label label={maxIssuesPerMinuteLabel}>
            <input
              disabled={cantSetUserLimit}
              type="number"
              value={userInfoLimits.maxIssuesPerMinute}
              onChange={updateStateLimits("maxIssuesPerMinute")}
            />
          </Label>
          <Label label={maxIssuesPerHourLabel}>
            <input
              disabled={cantSetUserLimit}
              type="number"
              value={userInfoLimits.maxIssuesPerHour}
              onChange={updateStateLimits("maxIssuesPerHour")}
            />
          </Label>
          <Label
            label="Account locked"
            helpUrl={`${docsAccountLockingUrl}/account-locking`}
            inline={true}
          >
            <input
              disabled={cantSetUserLimit}
              type="checkbox"
              defaultChecked={userInfoLimits.locked}
              onChange={() => setUserInfoLimits((state) => { return {
                ...state,
                locked: !state.locked
              }})}
            />
            {' '}
          </Label>
          <button
            className="btn"
            type="submit"
            disabled={isLoading}
          >
            <OkIcon /> Update limits
          </button>
        </form>
        <h2><HelpIcon /> Extra information</h2>
        <p>Signup IP: <b>{userInfo.ip || 'not set'}</b></p>
        <p>Nested set needs update (nestedSetNeedsUpdate): <b>{user0.nestedSetNeedsUpdate.toString()}</b></p>
        {loggedInUser.admin &&
          <p>Verified: <b>{user0.verified.toString()}</b></p>
        }
      </>
    </div>
  </>
};

export default Settings;

import { getLoggedInUser } from 'back'
import { cant } from 'front/cant'
import { formatDate } from 'front/date'

export async function getServerSideProps(context) {
  const { params: { uid }, req, res } = context
  if (
    typeof uid === 'string'
  ) {
    const sequelize = req.sequelize
    const [loggedInUser, user] = await Promise.all([
      getLoggedInUser(req, res),
      sequelize.models.User.findOne({
        where: { username: uid },
      }),
    ])
    if (!user) { return { notFound: true } }
    const props: SettingsProps = {}
    if (!loggedInUser) {
      return {
        redirect: {
          destination: routes.userNew(),
          permanent: false,
        }
      }
    }
    if (cant.viewUserSettings(loggedInUser, user)) {
      return { notFound: true }
    } else {
      ;[props.user, props.loggedInUser] = await Promise.all([
        user.toJson(loggedInUser),
        loggedInUser.toJson(loggedInUser),
      ])
    }
    return { props }
  } else {
    return { notFound: true }
  }
}