GithubHelp home page GithubHelp logo

senecajs / seneca-user Goto Github PK

View Code? Open in Web Editor NEW
21.0 13.0 29.0 1.57 MB

User account business logic (Seneca microservice component)

Home Page: https://senecajs.org

License: MIT License

JavaScript 100.00%
seneca microservices user business-logic component

seneca-user's Introduction

Seneca

A Seneca.js user management plugin.

@seneca/user

npm version build Coverage Status Maintainability

Voxgig This open source module is sponsored and supported by Voxgig.

Description

This module is a plugin for the Seneca framework. It provides a set of common user management actions (register, login etc.).

Install

npm install seneca
npm install seneca-promisify // dependency
npm install seneca-entity // dependency
npm install @seneca/user

Quick example

Register a user and then create an automatic login for testing.

const Seneca = require('seneca')

var seneca = Seneca()
  .use('promisify')
  .use('entity')
  .use('user')

var out = await seneca.post('sys:user,register:user', {
  handle: 'alice'
})

console.log('USER:', out.user)

out = await seneca.post('sys:user,login:user', {
  handle: 'alice',
  auto: true
})

console.log('LOGIN:', out.login)

Detailed Examples

Because Seneca treats messages as first-class citizens, 90% of unit testing can be implemented with message scenarios that also provide detailed usage examples:

Action Patterns

Action Descriptions

« adjust:user,sys:user »

Adjust user status idempotently (activated, etc.).

Parameters

  • active : boolean {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if user found',
  user: 'user entity'
}

« auth:user,sys:user »

Authenticate a login using token

Parameters

  • token : string {presence:required}
  • user_fields : array {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if login is active',
  user: 'user entity',
  login: 'user entity'
}

« change:pass,sys:user »

Change user password.

Parameters

  • pass : string
  • repeat : string {presence:optional}
  • verify : string {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if changed',
  user: 'user entity'
}

« change:handle,sys:user »

Change user handle.

Parameters

  • new_handle : string
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if changed',
  user: 'user entity'
}

« change:email,sys:user »

Change user email.

Parameters

  • new_email : string
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if changed',
  user: 'user entity'
}

« change:password,sys:user »

Change user password.

Parameters

  • pass : string
  • repeat : string {presence:optional}
  • verify : string {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if changed',
  user: 'user entity'
}

« check:verify,sys:user »

Check a verfication entry.

Parameters

  • kind : string {presence:optional}
  • code : string {presence:optional}
  • now : number {presence:optional}
  • expiry : boolean {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if valid',
  why: 'string coded reason if not valid'
}

« check:exists,sys:user »

Check user exists.

Parameters

  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if user exists',
  user: 'user entity'
}

« cmd:encrypt,hook:password,sys:user »

Encrypt a plain text password string.

Examples

  • cmd:encrypt,hook:password,sys:user,pass:foofoobarbar
    • Result: {ok:true, pass:encrypted-string, salt:string}

Parameters

  • salt : string {presence:optional}
  • pass : string {presence:optional}
  • password : string {presence:optional}
  • rounds : number {presence:optional}

Replies With

{
  ok: '_true_ if encryption succeeded',
  pass: 'encrypted password string',
  salt: 'salt value string'
}

« cmd:pass,hook:password,sys:user »

Validate a plain text password string.

Examples

  • cmd:pass,hook:password,sys:user,pass:goodpassword
    • Result: {ok:true}

Parameters

  • salt : string
  • pass : string
  • proposed : string
  • rounds : number {presence:optional}

Replies With

{
  ok: '_true_ if password is valid',
  why: 'string coded reason if not valid'
}

« get:user,sys:user »

Get user details

Parameters

  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if user found',
  user: 'user entity'
}

« list:user,sys:user »

List users

Parameters

  • active : boolean {presence:optional}
  • q : object {presence:optional}

Replies With

{
  ok: '_true_ if user found',
  items: 'user entity item list'
}

« list:login,sys:user »

List logins for a user

Parameters

  • active : boolean {presence:optional}
  • login_q : object {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if user found',
  items: 'user entity item list'
}

« list:verify,sys:user »

Create a verification entry (multiple use cases).

Parameters

  • kind : string
  • code : string {presence:optional}
  • once : boolean {presence:optional}
  • valid : boolean {presence:optional}
  • custom : object {presence:optional}
  • expire_point : number {presence:optional}
  • expire_duration : number {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if user found',
  verify: 'verify entity'
}

« login:user,sys:user »

Login user

Parameters

  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}
  • auto : boolean {presence:optional}
  • pass : string {presence:optional}

Replies With

{
  ok: '_true_ if user logged in',
  user: 'user entity',
  login: 'login entity'
}

« logout:user,sys:user »

Login user

Parameters

  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}
  • token : string {presence:optional}
  • login_in : string {presence:optional}
  • login_q : object {presence:optional,default:{}}
  • load_logins : boolean {presence:optional}

Replies With

{
  ok: '_true_ if user logged in',
  count: 'number of logouts'
}

« make:verify,sys:user »

Create a verification entry (multiple use cases).

Parameters

  • kind : string
  • code : string {presence:optional}
  • once : boolean {presence:optional}
  • valid : boolean {presence:optional}
  • custom : object {presence:optional}
  • expire_point : number {presence:optional}
  • expire_duration : number {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if user found',
  verify: 'verify entity'
}

« register:user,sys:user »

Register a new user

Parameters

  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • user : object {unknown:true}
  • user_data : object {unknown:true}

Replies With

{
  ok: '_true_ if user registration succeeded',
  user: 'user entity'
}

« remove:user,sys:user »

Remove a user

Parameters

  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if user removed',
  user: 'user entity'
}

« sys:user,update:user »

Update a user

Parameters

  • user : object {presence:optional}
  • id : string {presence:optional}
  • user_id : string {presence:optional}
  • email : string {presence:optional}
  • handle : string {presence:optional}
  • nick : string {presence:optional}
  • q : object {presence:optional}
  • fields : array {presence:optional}

Replies With

{
  ok: '_true_ if user updated',
  user: 'user entity'
}

License

Copyright (c) 2010-2020, Richard Rodger and other contributors. Licensed under MIT.

seneca-user's People

Contributors

adrieankhisbe avatar butlerx avatar ckiss avatar floridemai avatar geek avatar georgigriffiths avatar guyellis avatar mcdonnelldean avatar mcollina avatar mihaidma avatar mirceaalexandru avatar nherment avatar otaviosoares avatar paolochiodi avatar rithdmc avatar rjrodger avatar shanel262 avatar sorin-silaghi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

seneca-user's Issues

Security Label

A security label should be used to flag issues concerning security issues.

cf #8

hide logs of expected errors

hide logs of expected errors

// TODO hide logs of expected errors

  make_seneca: () => {
    // TODO hide logs of expected errors
    var seneca = Seneca({ legacy: false })
      .test()
      .use('promisify')
ndex 231d521..bf55c33 100644
++ b/test/verify.calls.js

create salt using a seneca action

If salt will be created using a seneca action, then it will be easy to implement a custom salt generator in case this is required.

An example is when the PostgreSQL is used the salt can contain characters that are invalid for text field and because of this error occurs.

Salt can be provided explicitly as part of create_user/change_password opertions, but if the salt generator is customized it ca be easier.

SHA512 vs bcrypt

Is there a reason you've chosen to go with SHA512 here, instead of bcrypt for password hashing?

to Issue"

to Issue"

- name: "TODO to Issue"

jobs:
  build:
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@master"
      - name: "TODO to Issue"
        uses: "alstr/todo-to-issue-action@master"
        with:
          REPO: ${{ github.repository }}
          BEFORE: ${{ github.event.before }}
          SHA: ${{ github.sha }}
          TOKEN: ${{ secrets.GITHUB_TOKEN }}
          LABEL: "TODO"
          COMMENT_MARKER: "//"
        id: "todo"

Option to remove login entries instead of marking as false

You may not want to keep the entries when you don't need to store previous login, it's interesting for recurrence and stats purpose, but it's a fast growing table
Instead of marking them as false, it should be possible to get them deleted or to keep only the n-latest ones

Example not working for seneca 2.x

The "Quick example", as it is in the README.md file, is not working, and "TypeError: seneca.make is not a function" is thrown. Details of the error is as follows:
2016-06-05T17:59:00.148Z s4a0n4bi0jgz/1465149540129/24391/- INFO hello Seneca/2.1.0/s4a0n4bi0jgz/1465149540129/24391/- 2016-06-05T17:59:00.581Z s4a0n4bi0jgz/1465149540129/24391/- ERROR act user OUT init:user 5 {init:user,tag:null} ENTRY (307yz0a2ewk3) - seneca: Action init:user failed: seneca.make is not a function. act_execute {id:roshwomznjf3/but3i64wamc0,gate:false,ungate:true,desc:null,time:{start:1465149540574},message:seneca.make i TypeError: seneca.make is not a function at Seneca.<anonymous> (/Users/icezee/TM/seneca/helloseneca/node_modules/seneca-user/user.js:1125:26)

To fix the issue, it needs to add use('entity'). Change the first lines of the example to the following should work:
var seneca = require('seneca')() seneca .use('entity') .use('user')

Update docs to clarify requirements of seneca-basic

The current docs on npm, which are linked from senecajs.org do not mention the requirement for seneca-basic. Using the example, as provided, results in the following cryptic explosion:

Seneca Fatal Error
==================

Message: seneca: No matching action pattern found for { list:    [ { entity: '-/sys/user', fields: [Object] },     { entity: '-/sys/login', fields: [] },     { entity: '-/sys/reset', fields: [] } ],  role: 'util',  cmd: 'define_sys_entity' }, and no default result provided (using a default$ property).

Code: act_not_found

Details: { args: '{ list:    [ { entity: \'-/sys/user\', fields: [Object] },     { entity: \'-/sys/login\', fields: [] },     { entity: \'-/sys/reset\', fields: [] } ],  role: \'util\',  cmd: \'define_sys_entity\' }' }

Stack:
    at Object.errormaker [as error] (C:\Code\seneca-tests\node_modules\eraro\eraro.js:94:15)
    at executable_action (C:\Code\seneca-tests\node_modules\seneca\seneca.js:1155:23)
    at execute_action (C:\Code\seneca-tests\node_modules\seneca\seneca.js:875:12)
    at Object.execspec.fn [as orig_fn] (C:\Code\seneca-tests\node_modules\seneca\seneca.js:1084:11)
    at Object.timeout_fn [as fn] (C:\Code\seneca-tests\node_modules\gate-executor\gate-executor.js:194:12)
    at next (C:\Code\seneca-tests\node_modules\gate-executor\gate-executor.js:126:14)
    at process (C:\Code\seneca-tests\node_modules\gate-executor\gate-executor.js:175:12)
    at Immediate.<anonymous> (C:\Code\seneca-tests\node_modules\gate-executor\gate-executor.js:258:7)
    at runCallback (timers.js:570:20)
    at tryOnImmediate (timers.js:550:5)

Instance: Seneca/2urn6ky9fqxp/1473197242606/17820/3.0.0/todo-user
  ALL ERRORS FATAL: action called with argument fatal$:true (probably a plugin init error, or using a plugin seneca instance, see senecajs.org/fatal.html)
    at executable_action (C:\Code\seneca-tests\node_modules\seneca\seneca.js:1160:22)

When: 2016-09-06T21:27:22.840Z

Log: {kind:null,plugin:seneca,tag:3.0.0,id:2urn6ky9fqxp/1473197242606/17820/3.0.0/todo-user,code:act_not_found,notic

And indeed, the current readme page doesn't really say much about it all outside of the quick example... I didn't even notice it until I looked through the README with a fine tooth comb.

@seneca/microcache - short duration cache of msg r...

@seneca/microcache - short duration cache of msg responses

<td class="source"> // TODO @seneca/microcache - short duration cache of msg responses</td>

                          <td class="source">  // TODO @seneca/audit - record user modifications - e.g activate</td>
                          
                        </tr>            <tr id="user.js__68" class="hit">
                          <td class="line" data-tooltip>68</td>
                          <td class="lint empty"></td>
                          <td class="hits" data-tooltip></td>
                          <td class="source">  // TODO @seneca/microcache - short duration cache of msg responses</td>
                          
                        </tr>            <tr id="user.js__69" class="hit">
                          <td class="line" data-tooltip>69</td>

Login brute force protection

Protect against brute force by having an option that enables a maximum number of allowed failed logins before account is locked.

The specs are the following:

  • The number of accepted failed attempts is provided through options,
  • By default the login failed attempts check is disabled,
  • One successful login resets the failed attempts counter,
  • One password reset resets the failed attempts counter,
  • The login will fail after the login failed attempts is reached, login failure is counted internally (not exposed through external commands).
  • An unlock cmd is exposed,
  • In sys_user the stored field is failedLoginCount. This is a breaking change.

/cc @mcdonnelldean @mirceaalexandru

Changelog :)

A changelog would be welcome for dev running upgrade over an old version, crawling thourgh the commits aint a pleasure :p

execute_reset does not work on version 0.2.8

In cmd_execute_reset when calling change_password, reset.user is passed as a parameter for user. This property contains the user database ID, not the user object as expected. Because of this the operation fails.

Missing seneca-basic dependency

I'm writing an application using seneca-user as user account backend service. I'm overriding some patterns to enforce security in a custom plugin.

import Seneca from 'seneca';
let s = Seneca();
s.use('entity');
//s.use("basic");
s.use("user");
s.ready(() => {
console.log("USER READY.... CONTINUING LOADING ")
s.use('./plugins/my-user-service'); // This service is overriding standard seneca-user functions
...

The application was starting correcly in previous version, but upgrading to the last version
"seneca": "^3.0.0",
"seneca-entity": "^1.3.0",
"seneca-mongo-store": "^1.1.0",
"seneca-user": "^2.1.0"
lead to the following exception at startup.

Seneca Fatal Error

Message: seneca: No matching action pattern found for { list: [ { entity: '-/sys/user', fields: [Object] }, { entity: '-/sys/login', fields: [] }, { entity: '-/sys/reset', fields: [] } ], role: 'util', cmd: 'define_sys_entity' }, and no default result provided (using a default$ property).

Looks like the dependency to seneca-basic is no more available ...

Uncommenting the line s.use("basic") solve the issue ... but the user plugin documentation is not up to date...

Regards,
Jérôme

The markdown of the readme is broken around login section

The docs detailing the login action are kind of broken. It looks like the markdown was not done properly resulting in things not looking right... see: https://github.com/senecajs/seneca-user#roleuser-cmdlogin

   * `nick_: required if no email, identifies user, alias: usernam`
   * _email_: required if no nick, identifies user
   * `password_: password as entered by user, optional if using _auto`
   * _auto_: automatic login without password, default: _false_. Use this to generate login tokens.
   * `user_: specify user using a sys/user entity - for convenience use inside your own actions, when you already have a user entity loade`

It appears emphasis was added to some of them, but the others it's inline code. Plus the last character is truncated on a few of them. The end result is it looks like this:

nick_: required if no email, identifies user, alias: usernam
email: required if no nick, identifies user
password_: password as entered by user, optional if using _auto
auto: automatic login without password, default: false. Use this to generate login tokens.
user_: specify user using a sys/user entity - for convenience use inside your own actions, when you already have a user entity loade

And that it makes it look like it accepts, e.g., a nick_ parameter which is not true - it's nick.

There's also some inconsistencies -- the execute_action uses emphasis while all others use inline code. I would think consistency is king here... Login looks to be a (messed up) combination of the two formats.

Salt shouldn't return a NULL character

When using the postgres connector, NULL character are not allowed, while still being generated by the salt:
"stack": "error: invalid byte sequence for encoding \"UTF8\": 0x00\n at Connection.parseE (/home/wardormeur/Public/CoderDojo/cp-local-development/workspace-zen/cp-users-service/node _modules/seneca-postgresql-store/node_modules/pg/lib/connection.js:539:11)\n at Connection.parseMessage (/home/wardormeur/Public/CoderDojo/cp-local-development/workspace-zen/cp-users -service/node_modules/seneca-postgresql-store/node_modules/pg/lib/connection.js:366:17)\n at Socket.<anonymous> (/home/wardormeur/Public/CoderDojo/cp-local-development/workspace-zen/ cp-users-service/node_modules/seneca-postgresql-store/node_modules/pg/lib/connection.js:105:22)\n at Socket.emit (events.js:95:17)\n at Socket.<anonymous> (_stream_readable.js:765 :14)\n at Socket.emit (events.js:92:17)\n at emitReadable_ (_stream_readable.js:427:10)\n at emitReadable (_stream_readable.js:423:5)\n at readableAddChunk (_stream_readable .js:166:9)\n at Socket.Readable.push (_stream_readable.js:128:10)", "query": { "text": "INSERT INTO sys_user (\"nick\",\"email\",\"name\",\"active\",\"when\",\"first_name\",\"last_name\",\"salt\",\"pass\",\"id\") values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)", "values": [ "[email protected]", "[email protected]", "tyeqfe aezfazfgeazfffffff", "true", "2016-07-21T15:58:41.977Z", "tyeqfe", "aezfazfgeazfffffff", "\u001a\u0000'\u001fE<>\u0002dc\u0000\u0003\u000b\u0010Cj", "475dd16ecfc365b9fd0bda46a7192e1acc44f919654c9ecd1b413167d79ee3a9134201465deff6ea1eea67076d7f0f796bf9e0545d6d1ed3b031ec1b1652d8a5", "9a0ea186-0e04-4292-8594-3449e8eafaaf" ] }

@seneca/audit - record user modifications - e.g ac...

@seneca/audit - record user modifications - e.g activate

<td class="source"> // TODO @seneca/audit - record user modifications - e.g activate</td>

                          <td class="hits" data-tooltip></td>
                          <td class="source">  // TODO @seneca/audit - record user modifications - e.g activate</td>
                          
                        </tr>            <tr id="user.js__68" class="hit">
                          <td class="line" data-tooltip>68</td>
                          <td class="lint empty"></td>
                          <td class="hits" data-tooltip></td>
                          <td class="source">  // TODO @seneca/microcache - short duration cache of msg responses</td>
                          
                        </tr>            <tr id="user.js__69" class="hit">
                          <td class="line" data-tooltip>69</td>

Security breach: Calling auth (or logout) without token can lead to { ok: true, ... }

In logout (this is where I detected the issue), no test is performed to ensure that a token has been provided.

  // Logout an existing user - resolve using token, idempotent
  // - token:    login token
  // Provides:
  // - success: {ok:true,user:,login:}
  function cmd_logout (args, done) {
    var seneca = this
    var userent = seneca.make(user_canon)
    var loginent = seneca.make(login_canon)


    var q = {id: args.token}

    loginent.load$(q, function (err, login) {

By providing no token in the args, the entity will load the first entity in the backend (tested with mongo). Which is not the user which is currently logging out. And the 'random user' will be logged out successfully!

The code appears to be the same in the auth implementation, which is leading to a security vulnerability.

A simple fix is just to set a default value for token (like null), to ensure the load will fail.

Clean saved user data

there is a method named cleanUser that removes useless data from the user object before saving. Need to verify all code that it cleans user before saving. Moreover unify all data cleaning/save to reduce code duplicates.

Miss of runtime dependency to seneca-promisify

This library is unusable if you have not loaded the seneca-promisify (and seneca-entity) plugin at startup.

Source code is referencing the seneca.message method which is uniquely added by promisify.

To prevent referencing directly seneca-promisify as dependency, using peerDependency could be a cleaner solution.

Logout fails if token is not found

Hello,

This simple code timeouts:

var seneca = require('seneca')();
seneca.use('user');
seneca.ready(function () {
  seneca.act({role:'user',cmd:'logout', token: "a6d6fb1a-219f"});   
});

It seems the issue is at this line: https://github.com/rjrodger/seneca-user/blob/master/user.js#L687
The function returns without calling the done callback.

      if( !login ) {
        return {ok:true}
      }

Perhaps should be something like:

      if( !login ) {
        return done(null,{ok:false})
      }

Thanks,
Yoann

seneca.alias method?</td>

seneca.alias method?

<td class="source"> // TODO seneca.alias method?</td>

                          <td class="hits" data-tooltip></td>
                          <td class="source">    // TODO seneca.alias method?</td>
                          
                        </tr>            <tr id="user.js__101" class="hit">
                          <td class="line" data-tooltip>101</td>

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.