Git Product home page Git Product logo

civil-server's Introduction

Civil Server

This is a node server, as a component that can be included in other projects and extended for use by other projects. It is spun out from the undebate project so that it can be used as a common component of many projects.

The idea is that Civil Server is a component with some basic funcationality that will be useful to a lot of projects. Some projects may take this and add more features and create a component out of that will be useful to other projects.

And when changes/improvements are made to this project, they can be easilly updated in other projects.

In addition, projects/repos that use the civil-server can be imported in other projects that use the civil-server, makeing it possible to break large server projects into smaller components, each of which can be build and tested separately.

Copyright 2021-2024 EnCiv, Inc. This work is licensed under the terms described in LICENSE.txt which is an MIT license with a Public Good License Condition.

Features

  • User Join/Login/Logout/Forgot Password to get you up and running quickly with user logins
  • MongoDB for extensible user database
  • React Server Sider Rendering for quick page loads
  • React Progressive Web Applications for interactive pages with low server load
  • React-Jss for inline styles
  • Socket.io for asyncronous, bidirectional API's
  • Server Events for extensibility communication between api's that generate events, and multiple handlers for them
  • Helmet for improved security
  • Webpack and nodemon for interactive development
  • Log4js for logging to a collection in MongoDB
  • Log4js from the browser for debugging
  • Loader.io verification for load testing

changes from 0.0.27 to 1.0.0

  • using Mongo-collection to replace mongo-models
  • for the models Iota, and User methods like .find() no longer return documents they return a cursor and .toArray() will work
  • the documents returned from methods like .findOne() or .toArray() are plain object, not of the User or Iota class. To make them of the class do new User(doc)
  • use User.validatePassord(doc,plainTextPassword) - user.validatePassord(plainTextPassord) no longer supported
  • use User.gererateKey(doc) - user.generateKey() no longer supported
  • use Iota.preload() - Iota.load is not supported

Getting Started

Follow these instructions to setup the civil-server repo: See Getting Started - Repo-Setup

Run it

npm run dev

You will now be able to go to http://localhost:3011

Contributing

When you are ready to contribute please see these notes:

How to use it

To create a new project from scratch

mkdir new-project
cd new-project
npm init #answer the questions as you want for your project
npm install github:EnCiv/civil-server
node_modules/.bin/do-civil

Your project directory is now ready for you.

npm run storybook and npm run dev will now work.

app/start.js

'use strict'

const path = require('path')
import { civilServer, Iota } from 'civil-server'
import iotas from '../iotas.json'
import App from './components/app'

Iota.preload(iotas)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
async function start() {
  try {
    const server = new civilServer()
    server.App = App
    await server.earlyStart()
    server.routesDirPaths.push(path.resolve(__dirname, './routes'))
    server.socketAPIsDirPaths.push(path.resolve(__dirname, './socket-apis'))
    server.serverEventsDirPaths.push(path.resolve(__dirname, './events'))
    await server.start()
    logger.info('started')
  } catch (error) {
    logger.error('error on start', error)
  }
}

start()

App

App is your outer wrapper React App for the whole web site. A minimal version looks like this:

import React from 'react'
import { hot } from 'react-hot-loader'
import WebComponents from '../web-components'
import Footer from './footer'
import ErrorBoundary from './error-boundary'

class App extends React.Component {
  render() {
    if (this.props.iota) {
      var { iota, ...newProps } = this.props
      Object.assign(newProps, this.props.iota)
      return (
        <ErrorBoundary>
          <div style={{ position: 'relative' }}>
            <WebComponents key="web-component" webComponent={this.props.iota.webComponent} {...newProps} />
            <Footer key="footer" />
          </div>
        </ErrorBoundary>
      )
    } else
      return (
        <ErrorBoundary>
          <div style={{ position: 'relative' }}>
            <div>Nothing Here</div>
            <Footer />
          </div>
        </ErrorBoundary>
      )
  }
}

export default hot(module)(App)

All of a sites pages will be based on WebComponents that are defined other React components as defined in app/web-components. You will learn more about this as we go, but for each page on the site, you will create an object in Iotas.json that says the path, and the WebComponent like this

[
  ...
  {
    "_id": {
      "$oid": "5d56e411e7179a084eefb365"
    },
    "path": "/join",
    "subject": "Join",
    "description": "Join the Civil Server",
    "webComponent": "Join"
  }
  ...
]

If a user browses to a path that matches, the webComponent named will be used to render the rest of the page, and the subject and description as passed to the webComponent as props.

Socket API Directory

Each file in the directory represents an api call. The root of the name of the file (eg socketlogger of socketlogger.js) is the name of the socket.io event that corresponding to the api. The paramaters of the api are determined by the definition of the function.

One api that is predefined in this module is socketlogger.js

'use strict'

function socketlogger(loggingEvent) {
  loggingEvent.data.push({ socketId: this.id, userId: this.synuser ? this.synuser.id : 'anonymous' })
  bslogger[loggingEvent.level.levelStr.toLowerCase()](loggingEvent.startTime, ...loggingEvent.data)
}

export default socketlogger

To call this API from the browser side you would use

window.socket.emit('socketlogger', loggingEvent)

an api function can have any number of parameters, it can also have a call-back as its final parameter.

Routes Directory

Each file in the directory represents an extension to the express server object - which can be this.app.use(...) this.app.get(...) or this.app.push(...) An example is the sign-in route that looks like this:

import expressRateLimit from 'express-rate-limit'
import sendUserId from '../util/send-user-id'

async function signIn(req, res, next) {
  try {
    const { password, ..._body } = req.body // don't let the password appear in the logs
  ...
  } catch(error){
    logger.error("signIn caught error",error)
  }
}

function route() {
  const apiLimiter = expressRateLimit({
    windowMs: 60 * 1000,
    max: 2,
    message: 'Too many attempts logging in, please try again after 24 hours.',
  })
  this.app.post('/sign/in', apiLimiter, signIn, this.setUserCookie, sendUserId)
}
export default route

The default function of the file will be called with this of the express object.

Events Dirctory

Note: Event's aren't used much and there may be better ways now.

Within the server, components can listen for and generate events. Each file in the events directory represents an event listener, and can define the name of an Event.

To create an event listener create a file in app/events like this:

import { serverEvents } from 'civil-server'

function eventListener(p1,p2,...){
 ...
}

serverEvents.on(serverEvents.eNames.EventName, eventListener)

In the code that is going to generate the event, do this:

import { Iota, serverEvents } from 'civil-server'

serverEvents.eNameAdd('EventName')

serverEvents.emit(serverEvents.eNames.EventName, p1, p2, ...)

Web Components Directory

Each file in web-components represents a React Component. When a url matches in the iota collection path property, the web-component named in the document is looked up in the Web Components directory, and is used to render the data in the document. After adding a new component to the directory and adding it to iotas.json, you will need to run npm install to update the auto generated index.js file in the directory.

Contributions

Contributions are accepted under the MIT License without additional conditions. Use of the software, however, must abide by the MIT License with the Public Good Condition. This additional condition ensures that in the event the software is sold or licensed to others, the revenue so generated will be used to further the public mission of EnCiv, Inc, a 501(c)(3) nonprofit, rather than to enrich any directors, employees, members, or shareholders. (Nonprofits can not have shareholders)

Getting started

You will need a github.com account, and a heroku.com account. Heroku is like a server in the cloud that you can push git repo's to, and after you push, heroku will build and run them. It's also great because they give you free accounts that you can use for development.

The install instructions are here

How to add a new web page to the server

Here is the flow. When a user visits the server with a url, getIota() in get-iota.js will look up the path in the database. If it finds a match, it will look for a webComponent property and then look for a matching component in the web-components directory and render that on the server through app/server/routes/serverReactRender. All the properties of this webComponent will be passed as props to the corresponding React component.Then the page will be sent to the browser, and then rehydrated there, meaning the webComponent will run again on the browser, starting at app/client/main-app.js and react will connect all the DOM elements.

1) Add a React Component to ./app/web-components

here is a simple one, ./app/web-components/undebate-iframes.js:

'use strict'

import React from 'react'
import injectSheet from 'react-jss'

const styles = {
  title: {
    color: 'black',
    fontSize: '2rem',
    textAlign: 'center',
  },
  frame: { marginTop: '1em', marginBottom: '1em', width: '100vw' },
}

class UndebateIframes extends React.Component {
  render() {
    const { classes } = this.props
    const width = typeof window !== 'undefined' ? window.innerWidth : 1920
    const height = typeof window !== 'undefined' ? window.innerHeight : 1080

    return (
      <div>
        <div>
          <span className={classes['title']}>These are the Undebates</span>
        </div>
        <iframe
          className={classes.frame}
          height={height * 0.9}
          width={width}
          name="race1"
          src="https://cc.enciv.org/san-francisco-district-attorney"
        />
        <iframe
          className={classes.frame}
          height={height * 0.9}
          width={width}
          name="race2"
          src="https://cc.enciv.org/country:us/state:wi/office:city-of-onalaska-mayor/2020-4-7"
        />
      </div>
    )
  }
}
export default injectSheet(styles)(UndebateIframes)

2) Create a new document in iotas.json

The example is the minimum information required. Any additional properties you add to webComponent will be passed as props to the associated React component.

[ ...,
  {
      "_id": {"$oid": "60d25dc95185ab71b8fa44a0"},
      "path": "/iframe-demo",
      "subject": "Iframe demo",
      "description": "a quick prototype of a page showing multiple undebates in separate iframes",
      "webComponent": {
          "webComponent": "UndebateIframes"
      },
  }
]

Note: use app/tools/mongo-id to create a new, unique mongo id and paste it in.

3) Advanced: Component

If your page should pull data out of the database, or calculate something to pass to the web component, then you can add a component to app/data-components and then add a component: {component: YourComponentNane, ...} to the document above.

BROWSER_ENV

To pass ENV variables from the Node server to the browser, use

.bashrc:

export BROWSER_ENV=NODE_ENV,HOSTNAME

and

heroku config:set BROWSER_ENV=NODE_ENV,HOSTNAME -a app-name

or set it as a Config Var on heroku

Then you will be able to access them through process.env.NODE_ENV on the browser too. By default, process.env.NODE_ENV is set to 'development'

Do not use this to send secrets to the browser as they are not secret there

Testing

Jest tests

npm run test

Cypress tests

npm run cypress:headless or npm run cypress

civil-server's People

Contributors

beribak avatar ddfridley avatar epg323 avatar iamcrisb avatar ice1080 avatar luiscmartinez avatar mrnanosh avatar thong-pham avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

Forkers

kamui-fin ice1080

civil-server's Issues

if route throws error server crashes

For example, if the tempId route throws an error because the schema validation fails, the error percolates up and the server crashes.

Server should ignore errors from routes and throw up a page or something.

[2021-12-20T14:18:14.361] [INFO] node - { tempId: { email: '' } }
[1] C:\Users\David Fridley\git\EnCiv\unpoll\node_modules\@hapi\joi\lib\errors.js:202
[1]     const error = new Error(message);
[1]                   ^
[1]
[1] Error [ValidationError]: child "email" fails because ["email" is not allowed to be empty]
[1]     at Object.exports.process (C:\Users\David Fridley\git\EnCiv\unpoll\node_modules\@hapi\joi\lib\errors.js:202:19)
[1]     at internals.Object._validateWithOptions (C:\Users\David Fridley\git\EnCiv\unpoll\node_modules\@hapi\joi\lib\types\any\index.js:763:31)
[1]     at internals.Object.validate (C:\Users\David Fridley\git\EnCiv\unpoll\node_modules\@hapi\joi\lib\types\any\index.js:797:21)
[1]     at Function.validate (C:\Users\David Fridley\git\EnCiv\unpoll\node_modules\mongo-models\index.js:462:28)
[1]     at new MongoModels (C:\Users\David Fridley\git\EnCiv\unpoll\node_modules\mongo-models\index.js:22:41)
[1]     at new User (C:\Users\David Fridley\git\EnCiv\unpoll\node_modules\civil-server\dist\models\user.js:18:1)
[1]     at C:\Users\David Fridley\git\EnCiv\unpoll\node_modules\civil-server\dist\models\user.js:37:25 {
[1]   isJoi: true,
[1]   details: [
[1]     {
[1]       message: '"email" is not allowed to be empty',
[1]       path: [ 'email' ],
[1]       type: 'any.empty',
[1]       context: { value: '', invalids: [ '' ], key: 'email', label: 'email' }
[1]     }
[1]   ],
[1]   _object: {
[1]     email: '',
[1]     password: '$2b$10$kVXYR7vgSSj4zUJRICITiuWDRjCpEn2iG/keS2Zq5kFh1uR4WbUd2'
[1]   },
[1]   annotate: [Function (anonymous)]
[1] }
[2] <e> [webpack-dev-server] [HPM] Error occurred while proxying request localhost:3011/tempid to http://localhost:3012/ [ECONNRESET] (https://nodejs.org/api/errors.html#errors_common_system_errors)
[1] [nodemon] app crashed - waiting for file changes before starting...
[2] <e> [webpack-dev-server] [HPM] Error occurred while proxying request localhost:3011/socket.io/?EIO=4&transport=polling&t=NtPx7t6&sid=Cb9uz3j6B8W6_aDgAAAA to http://localhost:3012/ [ECONNREFUSED] (https://nodejs.org/api/errors.html#errors_common_system_errors)
[2] <e> [webpack-dev-server] [HPM] Error occurred while proxying request localhost:3011/socket.io/?EIO=4&transport=polling&t=NtPx7t7&sid=Cb9uz3j6B8W6_aDgAAAA to http://localhost:3012/ [ECONNREFUSED] (https://nodejs.org/api/errors.html#errors_common_system_errors)

refactor mongo-models out to use @EnCiv/mongo-collections

Mongo-models is not supported - and getting out of date.
@EnCiv/mongo-collections was created to replace it.

  • check the status of tests before making any changes
  • app/models/iota to use Collection
  • app/models/user to use Collection
  • app/models/log to use Collection
  • find places that use mongo-models and update to use Mongo
  • upgrade to "mongodb": "^5.9.2",
  • upgrade jest, jest-mongodb and others jest related to match the civil-pursuit repo
  • create jest tests for log
  • ensure jest tests for user and iota pass
  • ensure other tests pass,

password reset key from email should be trimmed

If I cut and paste the key from my email, it has a space at the end. If I don't notice and try to reset my password, it fails and I can't see why.

  • can we fix the email so that I don't get a space at the end when I double click on the reset key
  • trim spaces off the beginning and end of the reset key the user enters on the reset password page

Markdown Docs does not work

in app/routes there is app-mddoc.js which should render a martkdown file when someone goes to /doc/markdownfile.md but it is not working. This has never been working in the civil-server so don't assume anything.

  • create assets/doc/example.md uses some simple markdown commands
  • navigate to localhost:3011/doc/example.md - this should render it. but now you get
  • as far as I can tell, the line 8 of app-mddoc.js is never getting executed
  • create a cypress test for this

Cypress Join and Login tests

Cypress has not been used since splitting the civil-server out of undebate. And, cypress has been updated to 29 due to security alerts.
Jest has also been updated.

  • Get Cypress and Jest working of this repo
  • Create a Cypress test for the join page that checks if a new user can create an account
  • Create a Cypress test for the login page that checks if an existing user can login
  • Create other cypress tests for the Join component and authform

jest test for app/routes/sign-in

Create a jest test for this. See app/models/test/user.js for how to startup the database.
Initialize the db with a user - use the password has from the above

  • test that user can log in with valid password
  • test that user can't log in with invalid password
  • test that user can't log in if email not found.
  • other tests welcome

GDPR Cookie Consent

When the user first visits our website, we need them to agree to cookies.

  • Use vanilla-cookieconsent

  • app/server/routes/react-server-render will need to be modified so that the things that require cookies are not run, but that code is incorporated into cookie-consent

  • Create a new model using mongo-collections for consent. Use that to store consent.

  • think about, but not required, how a new module could add additional things that require consent, without having to edit an existing module.

Per Repo server-react-render and main.js

If a repo like undebate-ssp is including the undebate and the civil-server, pages that get rendered by the complete build import one main.js file that has all the logic for everything. But if a user visits a page that's just part of undebate then the main.js contains more than is necessary.

For example if one goes to cc.enciv.org/undebates they get the top level page for undebate-ssp. But if one visits https://cc.enciv.org/san-francisco-district-attorney they get a page from the undebate repo - but it's rendered by App from undebate-ssp and both have a common main.js

When a projects server starts - in app/start.js there is the code

import App from './components/app'
   ...
   server.App=App

This is where the App from the repo of the top level project is being set.

In addition, all pages of the different repos/projects are rendered with on common app/components/app.js wrapper and use the same
app/server/routes/server-react-render - though maybe this is okay and we need to just factor our what needs to be project/repo specific, it's possible that this should be per project rather than the same for all cases.

socketlogger can get overwhelmed - need to sanitize the data

In the undebate repo, ending.jsx this line:

    logger.trace('ending.onUserUpload', props)

will cause the socket.io websocket to disconnect after several seconds.
The disconnect will cause the node-socketio-stream in create-participant.js to close, and close the socket.io stream. Note: if the socket.io-stream didn't force a close of the socket.io socket - it will reconnect.
But the end result was that uploads of recorded video would get aborted a few seconds after they are started and it was really hard to trace down.

The problem was that in ending.jsx, the props includes ccState which includes the recorded video blobs. If you console.info props, its' fine and it shows the blob size. But (I presume) that socket.io is trying to encode and transfer the video blobs (they are megabytes).

The proposed solution here is that socketlogger on the client side, should traverse through the arguments being logged, and convert Blobs and other large things into strings that just indicate the length. (similar to how it looks with console.info). For example Blob {size:1743098, type: 'video/webm;codecs=vp9,opus'}

OR there might be an option to tell socket.io to do this. But it needs to be investigated, and must only apply to the socketlogger api calls and not other api calls.

The work around is to not log props in this case, but it's hard to prevent this type of error from coming up again.

Please build a jest test for this. It's easy to create tests of the socket-apis that have both the client and server side code and it makes it a lot easier to write and test the code for a deep feature like this - see undebate-ssp /app/socket-apis/tests/send-moderator-invite.js for an example.

jest test for app/socket-api/send-password.js

need jest tests for the sendPassord function in this file, great to also test sendPasswordEmail
see app/models/test/user.js for how to initialize the database so that the User model will work.
for the text create a mock functions for SibGetTemplateId and SibSendTransacEmail to get this to working.

Validate email on new account

When a user creates an account, send them an email where they click on a link to validate their email address.

Only valid for a programmable number of minutes. Then they have to issue a new link to validate.

UX -
When the user uses the AuthForm to join, after they hit join the are given a "Welcome" message.
Instead that message should say "Check you email for a confirmation message".
There should also be a button saying "Resend"
The user will receive a message that has a link to click on.
The user clicks on that link and a new page/tab opens up. (Consider that the user could be opening the email on their phone, while they are joining from the browser on their laptop).
The user taps on a button that says "Confirm"
That tab/window closes.
In the original tab window, the status message changes to "Welcome" and then goes to the next page.

Need to break up main.js for faster fist page load

Currently the civil-server is creating one all encompassing main.js file. It's about 2Megs right now, which is larger than what is recommended.

We are using webpack to build this file, but webpack has many features for creating multiple script files, and for lazy loading.

Find a way to break up the main.js file so that the initial load is smaller but all that is needed eventually gets loaded.

Note that civil-server first renders the code on the server side (through app/server/routes/server-react-render) and after the user views it, the code on the browser is rehydrated from main.js. It's the code that pertains to the first page rendered that the user wants loaded as quickly as possible so it can be rehydrated quickly and the user can then interact with it. But also consider that since the initial page has content, the user spends time looking at it before needing to interact with it and so our single large main.js file hasn't been too much of a problem.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.