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,
  docsUrl,
  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 maxArticlesLabel = "Maximum number of articles, issues and comments (maxArticles)"
const maxArticleSizeLabel = "Maximum article/issue/comment size (maxArticleSize)"
const maxUploadsLabel = "Maximum number of uploads (maxUploads)"
const maxUploadSizeLabel = "Maximum upload size (maxUploadSize)"
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)
    }
  }

  // Limits.
  const [userInfoLimits, setUserInfoLimits] = React.useState(lodash.pick(
    user0,
    [
      'locked',
      'maxArticles',
      'maxArticleSize',
      'maxUploads',
      'maxUploadSize',
      '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}
            helpUrl={`${docsUrl}/ourbigbook-web-hide-article-dates`}
          >
            <input
              type="checkbox"
              defaultChecked={userInfo.hideArticleDates}
              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>
        {cantSetUserLimit &&
          <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.maxArticles}
              onChange={updateStateLimits("maxArticles")}
            />
          </Label>
          <Label label={maxArticleSizeLabel}>
            <input
              disabled={cantSetUserLimit}
              type="number"
              value={userInfoLimits.maxArticleSize}
              onChange={updateStateLimits("maxArticleSize")}
            />
          </Label>
          <Label label={maxUploadsLabel}>
            <input
              disabled={cantSetUserLimit}
              type="number"
              value={userInfoLimits.maxUploads}
              onChange={updateStateLimits("maxUploads")}
            />
          </Label>
          <Label label={maxUploadSizeLabel}>
            <input
              disabled={cantSetUserLimit}
              type="number"
              value={userInfoLimits.maxUploadSize}
              onChange={updateStateLimits("maxUploadSize")}
            />
          </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}
            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>{user0.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'

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 }
  }
}