NML Says

Open Source Development 2

References for this Part

Model Solutions Previous Lesson

OSD.1.0

JavaScript

Example 0. Testsuite. test-suite.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
/*
        test-suite.js --        tests for primes

        Copyright (c) Niels Müller Larsen
        Licensed under the MIT license: https://opensource.org/license/mit
*/

const test = require('node:test');              // new in node as of V18
const assert = require('assert/strict');

const Rational = require('./Rational');         // to be tested

test('gcd(6,14)', function() {
    return assert.equal(Rational.gcd(6,14), 2);
});

test('no args', function() {
        let f = new Rational();                 // arrange
        let s = f.toString();                   // act
        return assert.equal(s, '0');    // assert
});

test('3', function() {
        let f = new Rational(3);
        return assert.equal(f.toString(), '3');
});

test('3/7', function() {
        let f = new Rational(3, 7);
        return assert.equal(f.toString(), '3/7');
});

test('-3/7', function() {
        let f = new Rational(-3, 7);
        return assert.equal(f.toString(), '-3/7');
});

test('3/-7 => -3/7', function() {
        let f = new Rational(3, -7);
        return assert.equal(f.toString(), '-3/7');
});

test('6/-14 => -3/7', function() {
        let f = new Rational(6, -14);               // tests reduce
        return assert.equal(f.toString(), '-3/7');
});

test('-3', function() {
        let f = new Rational(-3);
        return assert.equal(f.toString(), '-3');
});


test('3/7 + 1/8 => 31/56', function() {
        let f = new Rational(3, 7);
        let o = new Rational(1, 8);
        return assert.equal(f.add(o).toString(), '31/56');
});

test('3/7 - 1/8 => 17/56', function() {
        let f = new Rational(3, 7);
        let o = new Rational(1, 8);
        return assert.equal(f.sub(o).toString(), '17/56');
});

test('3/7 * 1/8 => 3/56', function() {
        let f = new Rational(3, 7);
        let o = new Rational(1, 8);
        return assert.equal(f.mul(o).toString(), '3/56');
});

test('3/7 / 1/8 => 24/7', function() {
        let f = new Rational(3, 7);
        let o = new Rational(1, 8);
        return assert.equal(f.div(o).toString(), '24/7');
});

test('3/4 > 2/3 -> 1', function() {
    let f = new Rational(3, 4);
    let o = new Rational(2, 3);
    return assert.equal(f.cmp(o), 1);
});

test('2/3 = 2/3 -> 0', function() {
    let f = new Rational(2, 3);
    let o = new Rational(2, 3);
    return assert.equal(f.cmp(o), 0);
});

test('2/3 < 3/4 -> -1', function() {
    let f = new Rational(2, 3);
    let o = new Rational(3, 4);
    return assert.equal(f.cmp(o), -1);
});

test('clone 3/7 with 6/7 / 2', function() {
    let f = new Rational(3, 7);
    let o = new Rational(6, 7);
    let p = new Rational(2);
    let q = o.div(p);
    return assert.equal(q.cmp(f), 0);
});

test('2/3 is', function() {
    let f = new Rational(2, 3);
    let o = f;
    return assert.equal(f == o, true);
});
Example 1. Rational. Rational.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/*
 * Rational.js
 *
 * Copyright 2025 (c) Niels Müller Larsen
 * Licensed under the MIT license: https://opensource.org/license/mit
 */

const isInt = function (a) {
    let w = parseFloat(a);
    return !isNaN(a) && (w | 0) === w;
}

module.exports =  class Rational {

    constructor(n, d) {
        this.numerator = n ? Number(n) : 0;
        this.denominator = d ? Number(d) : 1;
        if (this.denominator < 0) {         // change signs if d neg
            this.denominator = this.denominator - (this.denominator * 2);
            this.numerator = this.numerator - (this.numerator * 2);
        }
        this.reduce();
    }

    clone() {
        return new Rational(this.numerator, this.denominator);
    }
    
    cmp(o) {
        if (this.numerator / this.denominator < o.numerator / o.denominator) 
            return -1;
        else if (this.numerator / this.denominator > o.numerator / o.denominator)
            return 1;
        return 0;
    }

    static gcd(a, b) {
        if (b == 0)
            return a;
        else
            return Rational.gcd(b, a % b);
    }

    invert() {
        if (this.numerator === 0)
            return new Rational();
        return new Rational(this.denominator, this.numerator);
    }

    negate() {
        return new Rational(this.numerator - (this.numerator * 2), this.denominator);
    }
        
    reduce() {
        let div = Rational.gcd(this.numerator, this.denominator);
        this.numerator /= div;
        this.denominator /= div;
    }

    add(o) {
        let nn = this.numerator * o.denominator;
        nn += o.numerator * this.denominator;
        return new Rational(nn, this.denominator * o.denominator);
    }

    div(o) {
        return this.mul(o.invert());
    }

    mul(o) {
        return new Rational(this.numerator * o.numerator, this.denominator * o.denominator);
    }
    
    sub(o) {
        return this.add(o.negate());
    }

    toString() {
        if (this.numerator === 0)
            return '0';
        else if (this.denominator === 1)
            return `${this.numerator}`;
        return `${this.numerator}/${this.denominator}`;
    }
}
Example 2. Run Tests
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ node --test
✔ gcd(6,14) (1.847295ms)
✔ no args (0.338948ms)
3 (0.257716ms)
✔ 3/7 (0.186308ms)
✔ -3/7 (0.192404ms)
✔ 3/-7 => -3/7 (0.167528ms)
✔ 6/-14 => -3/7 (0.282852ms)
✔ -3 (0.178285ms)
✔ 3/7 + 1/8 => 31/56 (0.334924ms)
✔ 3/7 - 1/8 => 17/56 (2.289137ms)
✔ 3/7 * 1/8 => 3/56 (0.340437ms)
✔ 3/7 / 1/8 => 24/7 (0.250784ms)
✔ 3/4 > 2/3 -> 1 (0.313637ms)
✔ 2/3 = 2/3 -> 0 (0.284581ms)
✔ 2/3 < 3/4 -> -1 (0.223729ms)
✔ clone 3/7 with 6/7 / 2 (0.256303ms)
✔ 2/3 is (0.277696ms)
ℹ tests 17
ℹ suites 0
ℹ pass 17
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 139.064998

Python

Example 3. Testsuite. testsuite.py
Example 4. Rational. Rational.py
Example 5. Run Tests

A FOSS Application to be Improved

Generating a Dynamic Express Site

Normally, probably, we generate dynamic, not static, sites. This is what most web development is about. Doing that we must reference a templating engine that is capable of interpolating variable dynamic data into an HTML5 template. Because there is a choice of templating engines we denominate one, pug. The default is, strangely, an ancestor of pug, jade that nobody uses any more.

Example 6. Create Dynamic Express Site
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 $ npx express-generator -v pug --git firstlocal

   create : firstlocal/
   create : firstlocal/public/
   create : firstlocal/public/javascripts/
   create : firstlocal/public/images/
   create : firstlocal/public/stylesheets/
   create : firstlocal/public/stylesheets/style.css
   create : firstlocal/routes/
   create : firstlocal/routes/index.js
   create : firstlocal/routes/users.js
   create : firstlocal/views/
   create : firstlocal/views/error.pug
   create : firstlocal/views/index.pug
   create : firstlocal/views/layout.pug
   create : firstlocal/.gitignore
   create : firstlocal/app.js
   create : firstlocal/package.json
   create : firstlocal/bin/
   create : firstlocal/bin/www

   change directory:
     $ cd firstlocal

   install dependencies:
     $ npm install

   run the app:
     $ DEBUG=firstlocal:* npm start

$ cd firstlocal
$ git init
$ npm i
$ npm audit fix --force
$ npm audit fix --force
$ DEBUG=firstlocal:* npm start

In your browser navigate to http://localhost:3000 and see the welcome page.

In order to be up-to-date with JavaScript and practical work, first edit package.json

Example 7. Edited package.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ cat package.json
{
  "name": "firstlocal",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www",
    "pretest": "npm install",
    "test": "DEBUG=firstlocal:* node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "express": "^4.21.2",
    "http-errors": "~1.6.3",
    "morgan": "~1.9.1",
    "pug": "^3.0.3"
  }
}

Lines 8 and 9 are new, and allow you to start a testrun with npm test. This will install all dependencies, and then start the server.

Then modernize the first lines of the app.js setup file so that all occurrences of var are replaced by const. It must look similar to the following:

Example 8. The First Lines of the Essential app.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
require('dotenv').config();

const cookieParser = require('cookie-parser');
const cors = require('cors');
const createError = require('http-errors');
const express = require('express');
const helmet = require('helmet');
const logger = require('morgan');
const path = require('path');

const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');

const app = express();

// view engine setup 
// ... rest untouched

The first line is new, actually optional, but needed more often than not.

Requiring cors, re https://www.npmjs.com/package/cors, and helmet, re https://www.npmjs.com/package/helmet are essentially security related and should be considered in any project.

I have ordered the lines so that the external requirements are sorted alphabetically. Makes them easier on the maintainers eye.

JWT, Json Web Tokens

In order to safeguard security in a web application the user must authenticate. For regular applications this results in the creation of a session. A session is signified by a token, a variable, that is created at succesful login, and it will exist until the user logs out. Its existence will be checked when privileged activities in the application require login.

When web applications are using the REST API, the users must still authenticate to achieve certain privileged permissions, but the session is sometimes replaced by a token that, by and large, has the same function as a session cookie. The formalities behind these token are found in [JSON Web Token (JWT) Response for OAuth Token Introspection)(https://www.rfc-editor.org/rfc/rfc9701.txt)

In node application we have an implementation in https://www.npmjs.com/package/jsonwebtoken that may be installed by npm i jsonwebtoken.

The latter link has several usage examples. A token should be created at succesful authentication by code similar to

Example 9. Create Json Web Token. From coursecode/eduapi
1
2
3
4
5
6
7
8
9
const jwt = require("jsonwebtoken");

authUser(req, res, next) {
    // the user has just had password verified
    const payload = { uid: user.uid };
    const lifetime = { expiresIn: 3600 };
    let token = await jwt.sign(payload, process.env.SECRET, lifetime);
    // ...
},
Example 10. Verify Token
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
isAuth: async function (req, res, next) {

    ...
    let token = req.headers.authorization.split(' ')[1];
    if (!token)
        throw new Error(errmsg);

    errmsg = 'Failed to authenticate token';
    let jwt = await jwt.verify(token, process.env.SECRET);
    if (!res)
        throw new Error(errmsg);

    // in casu jwt.uid holds the payload
    // if the lifetime of the token has not expired
    // the existence of the token should grant access
    ...
},

Both code fragments are from a controller function. The variable process.env.SECRET will be a cat’s footprint kept in the .env file of the application.

ses

The rules for handing in assignments may be found in the README

Exercise DS.6.0

Todays exercise will consist of completing a partly done REST API application.

The skeleton application is to be forked from

https://codeberg.org/arosano/restapi.git

The Database sampleAPI.db

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
sqlite> .tables
city             country          user           
continent        countrylanguage

sqlite> .schema user
CREATE TABLE user (
        email varchar(64) unique not null,
        password blob not null,
        profile int not null default 99,
        bio text not null
);

sqlite> .schema continent
CREATE TABLE continent(
    name varchar(16) unique not null
);

sqlite> .schema country
CREATE TABLE country(
    code char(3) unique not null,
    name varchar(52) default null,
    continent varchar(16) not null,
    region varchar(26) default null,
    surfacearea float(10,2) not null default 0.0,
    indepyear int default null,
    population int not null default 0,
    lifeexpectancy float(3,1) default null,
    gnp float(10,2) default null,
    gnpold float(10,2) default null,
    localname varchar(45) default null,
    governmentform varchar(32) not null default '',
    headofstate varchar(32) default null,
    capital int default null,
    code2 char(2) unique not null,
    foreign key(continent) references continent(name)
);

sqlite> .schema city
CREATE TABLE city (
    name varchar(35) not null,
    countrycode char(3) not null,
    district varchar(20) default null,
    population int not null default 0,
    foreign key(countrycode) references country(code)
);

sqlite> .schema countrylanguage
CREATE TABLE countrylanguage (
    countrycode char(3) not null,
    language varchar(30) not null,
    isofficial boolean default 'false' not null,
    percentage float(4,1) not null default 0.0,
    primary key(countrycode, language),
    foreign key(countrycode) references country(code)
);

sqlite> select count(*) from user;
0
sqlite> select count(*) from continent;
7
sqlite> select count(*) from country;
239
sqlite> select count(*) from city;
4082
sqlite> select count(*) from countrylanguage;
991

Completion means:

  • In routes/users.js create whatever may be missing routes for
    • registering
    • logging in
  • Create controller function in controllersworld.js for adding a city to the database. Usage must require authentication, and admin privileges.
  • All reading and displaying of data must require authentication as at least a regular user.
  • Only an admin may change the entries in the user table ie change profiles. Controller functions must be in controllers.js

Logging in should mean receiving a token from the API server. You must rpobably save in in local storage, so that it can be used repeatedly until it’s expiration.

The architecture is MVC, Model-View-Controller. You must not change that.

Please keep the separation of functionality between the two routers.

This should probably be done in groups of two or three. Some research is expected. Take a couple of weeks to do it. And don’t forget that questions are welcome.