Merge branch 'master' into train-144-merge
|
@ -302,6 +302,11 @@ workflows:
|
|||
module: 123done
|
||||
requires:
|
||||
- install
|
||||
- build-module:
|
||||
name: fortress
|
||||
module: fortress
|
||||
requires:
|
||||
- install
|
||||
- build-module:
|
||||
name: browserid-verifier
|
||||
module: browserid-verifier
|
||||
|
@ -395,6 +400,16 @@ workflows:
|
|||
module: 123done
|
||||
requires:
|
||||
- install
|
||||
- deploy-module:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
name: fortress
|
||||
module: fortress
|
||||
requires:
|
||||
- install
|
||||
- deploy-module:
|
||||
filters:
|
||||
tags:
|
||||
|
|
|
@ -38,6 +38,12 @@ if grep -e "$MODULE" -e 'all' $DIR/../packages/test.list; then
|
|||
|
||||
cd ../fxa-shared
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
cd ../fxa-auth-server
|
||||
node scripts/gen_keys.js
|
||||
node scripts/gen_vapid_keys.js
|
||||
node fxa-oauth-server/scripts/gen_keys.js
|
||||
|
||||
cd ../fxa-content-server
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ PATH=$PATH:$HOME/.cargo/bin
|
|||
"cd fxa-profile-server; npm ci; mkdir -p var/public/" \
|
||||
"cd fxa-basket-proxy; npm ci" \
|
||||
"cd 123done; npm i" \
|
||||
"cd fortress; npm i" \
|
||||
"cd fxa-shared; npm ci" \
|
||||
"cd fxa-geodb; npm i" \
|
||||
"cd fxa-email-event-proxy; npm i" \
|
||||
|
|
|
@ -96,6 +96,17 @@
|
|||
},
|
||||
"min_uptime": "2m"
|
||||
},
|
||||
{
|
||||
"name": "Fortress PORT 9292",
|
||||
"script": "server.js",
|
||||
"cwd": "packages/fortress",
|
||||
"max_restarts": "1",
|
||||
"env": {
|
||||
"CONFIG_FORTRESS": "./config-local.json",
|
||||
"NODE_ENV": "dev"
|
||||
},
|
||||
"min_uptime": "2m"
|
||||
},
|
||||
{
|
||||
"name": "123done PORT 8080",
|
||||
"script": "server.js",
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
"moduleDependencies": {
|
||||
"fxa-content-server": [
|
||||
"123done",
|
||||
"fortress",
|
||||
"fxa-auth-server",
|
||||
"fxa-js-client",
|
||||
"fxa-shared",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"directory": "static/bower_components"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
*~
|
||||
node_modules
|
||||
public-key.json
|
||||
secret-key.json
|
||||
static/bower_components
|
|
@ -0,0 +1,2 @@
|
|||
static/**
|
||||
node_modules/**
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "prettier",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
public-key.json
|
||||
secret-key.json
|
||||
/node_modules
|
||||
static/bower_components
|
||||
*~
|
||||
*.log
|
|
@ -0,0 +1,9 @@
|
|||
LICENSE
|
||||
.*
|
||||
Dockerfile
|
||||
*.sh
|
||||
*.ico
|
||||
*.txt
|
||||
ansible/*
|
||||
static/bower_components/*
|
||||
static/img/*
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
FROM node:10-alpine
|
||||
|
||||
# as root
|
||||
RUN apk update
|
||||
RUN apk add g++ git
|
||||
RUN npm install -g bower
|
||||
|
||||
RUN addgroup -g 10001 app && adduser -D -G app -h /app -u 10001 app
|
||||
WORKDIR /app
|
||||
USER app
|
||||
|
||||
# as app
|
||||
COPY package.json package.json
|
||||
COPY bower.json bower.json
|
||||
COPY .bowerrc .bowerrc
|
||||
RUN npm install
|
||||
RUN /bin/rm -rf .npm
|
||||
|
||||
COPY . /app
|
||||
|
||||
USER root
|
||||
RUN apk del -r g++ git
|
||||
|
||||
CMD node ./server.js
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
## A demo of Product Relying party
|
||||
|
||||
## running locally
|
||||
|
||||
1. install [git] and [node]
|
||||
1. get a local copy of the repository: `git clone https://github.com/mozilla/fxa`
|
||||
1. `cd fxa/packages/fortress`
|
||||
1. install dependencies: `npm install`
|
||||
1. generate keys `node scripts/gen_keys.js`
|
||||
1. run the server: `npm start`
|
||||
1. visit it in your browser: `http://127.0.0.1:9292/`
|
||||
1. hack and reload! (web resources don't require a server restart)
|
||||
|
||||
[git]: http://git-scm.org
|
||||
[node]: http://nodejs.org
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "firefox-fortress",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/mozilla/123done",
|
||||
"authors": ["johngruen <john.gruen@gmail.com>"],
|
||||
"description": "fxa-oauth-demo",
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"app/bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"static/bower_components"
|
||||
],
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jquery": "2.1.0",
|
||||
"normalize-css": "3.0.1",
|
||||
"modernizr": "2.7.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"client_id": "dcdb5ae7add825d2",
|
||||
"client_secret": "b93ef8a8f3e553a430d7e5b904c6132b2722633af9f03128029201d24a97f2a8",
|
||||
"redirect_uri": "http://127.0.0.1:9292/download"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
var path = require('path');
|
||||
|
||||
var configTarget = process.env.CONFIG_FORTRESS || './config.json';
|
||||
var configFile = path.resolve(__dirname, configTarget);
|
||||
|
||||
var now = '[' + new Date().toISOString() + ']';
|
||||
console.log(now, 'loading configuration File', configFile); //eslint-disable-line no-console
|
||||
|
||||
var config = require(configFile);
|
||||
console.log(now, 'config:', JSON.stringify(config, null, 2)); //eslint-disable-line no-console
|
||||
|
||||
module.exports = config;
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"client_id": "dcdb5ae7add825d2",
|
||||
"client_secret": "b93ef8a8f3e553a430d7e5b904c6132b2722633af9f03128029201d24a97f2a8",
|
||||
"redirect_uri": "http://127.0.0.1:9292/download"
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "firefox-fortress",
|
||||
"description": "A simple tasklist app that demonstrates FxA Sign-In",
|
||||
"version": "0.0.2",
|
||||
"author": {
|
||||
"name": "Mozilla",
|
||||
"url": "https://mozilla.org/"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MPL 2.0",
|
||||
"url": "https://mozilla.org/MPL/2.0/"
|
||||
}
|
||||
],
|
||||
"homepage": "http://fortress.firefox.org/",
|
||||
"bugs": "https://github.com/mozilla/fxa/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mozilla/fxa.git"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"bower": "*",
|
||||
"client-sessions": "0.6.x",
|
||||
"express": "4.16.4",
|
||||
"morgan": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-prettier": "^5.0.0",
|
||||
"prettier": "^1.18.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "bower install --config.interactive=false -s",
|
||||
"start": "node server.js",
|
||||
"test": "eslint .",
|
||||
"format": "prettier '**' --write"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const sessions = require('client-sessions');
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
const logger = morgan('short');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(logger, express.json());
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
if (/^\/api/.test(req.url)) {
|
||||
res.setHeader('Cache-Control', 'no-cache, max-age=0');
|
||||
|
||||
return sessions({
|
||||
cookieName: config.cookieName || 'fortress',
|
||||
secret: process.env['COOKIE_SECRET'] || 'define a real secret, please',
|
||||
requestKey: 'session',
|
||||
cookie: {
|
||||
path: '/api',
|
||||
httpOnly: true,
|
||||
},
|
||||
})(req, res, next);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/download', function(req, res, next) {
|
||||
req.url = '/download.html';
|
||||
next();
|
||||
});
|
||||
|
||||
app.get(/^\/iframe(:?\/(?:index.html)?)?$/, function(req, res, next) {
|
||||
req.url = '/index.html';
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'static')));
|
||||
|
||||
const port = process.env['PORT'] || config.port || 9292;
|
||||
app.listen(port, '0.0.0.0');
|
||||
console.log('Firefox Fortress started on port', port); //eslint-disable-line no-console
|
|
@ -0,0 +1,277 @@
|
|||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f2f2f2;
|
||||
color: #424f59;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0095dd;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 960px;
|
||||
min-width: 320px;
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
/* Reset `button` default styles */
|
||||
button {
|
||||
-webkit-box-sizing: content-box;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
background: none;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
-webkit-user-select: none; /* for button */
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.banner {
|
||||
background: #304050;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #eef2f0;
|
||||
border-radius: 2px;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
#splash .btn {
|
||||
background: #5cae40;
|
||||
color: white;
|
||||
display: block;
|
||||
margin: 20px auto 0px;
|
||||
text-align: center;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#splash {
|
||||
font-family: 'Alegreya Sans', sans-serif;
|
||||
margin-bottom: 60px;
|
||||
margin-top: 150px;
|
||||
}
|
||||
|
||||
#splash header {
|
||||
font-weight: 300;
|
||||
background: #eef2f0;
|
||||
margin: 0 0 60px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#splash-logo {
|
||||
display: block;
|
||||
margin: 20px auto 0 auto;
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
#splash h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 100;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#splash h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
border-bottom: 1px dotted #ccc;
|
||||
clear: both;
|
||||
margin: 50px auto;
|
||||
padding: 0 0 50px 0;
|
||||
text-align: center;
|
||||
width: 720px;
|
||||
}
|
||||
|
||||
.left-col {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
.right-col {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.two-col h3 {
|
||||
font-size: 42px;
|
||||
font-weight: 300;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.two-col p {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#header-main {
|
||||
background: #fff;
|
||||
box-shadow: 0px 2px 2px #ccc;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#subscriptionCTA {
|
||||
float: right;
|
||||
margin: 7px 0;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.is-subscribed #subscriptionCTA {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#header-main h1 {
|
||||
float: left;
|
||||
font-size: 24px;
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 0 10px;
|
||||
position: relative;
|
||||
top: -8px;
|
||||
}
|
||||
|
||||
#header-main h1 .pro-status {
|
||||
display: none;
|
||||
}
|
||||
.is-subscribed #header-main h1 .pro-status {
|
||||
display: inline;
|
||||
}
|
||||
.is-subscribed #header-main {
|
||||
background-image: url(/img/pro-header-bg.gif);
|
||||
}
|
||||
|
||||
#footer-main {
|
||||
background: url(/img/grad@2x.png) repeat-x #304050;
|
||||
background-size: 5px 20px;
|
||||
bottom: 0;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
padding: 10px 0 0 0;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#footer-main p {
|
||||
font-size: 0.8em;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
background: url(/img/logo@2x.png) no-repeat center center;
|
||||
background-size: 36px 34px;
|
||||
display: inline-block;
|
||||
height: 35px;
|
||||
margin: 5px auto 0 auto;
|
||||
position: relative;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
div.logo {
|
||||
background-size: 26px 25px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 960px) {
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
width: 96%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#header-main {
|
||||
background: #f2f2f2;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (-webkit-min-device-pixel-ratio: 2),
|
||||
only screen and (min--moz-device-pixel-ratio: 2),
|
||||
only screen and (-moz-min-device-pixel-ratio: 2),
|
||||
only screen and (-o-min-device-pixel-ratio: 2/1),
|
||||
only screen and (min-device-pixel-ratio: 2),
|
||||
only screen and (min-resolution: 192dpi),
|
||||
only screen and (min-resolution: 2dppx) {
|
||||
#logo {
|
||||
background-image: url(/img/logo@2x.png);
|
||||
}
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
content: ' ';
|
||||
clear: both;
|
||||
height: 0;
|
||||
}
|
||||
.clearfix {
|
||||
display: inline-block;
|
||||
}
|
||||
/* start commented backslash hack \*/
|
||||
* html .clearfix {
|
||||
height: 1%;
|
||||
}
|
||||
.clearfix {
|
||||
display: block;
|
||||
}
|
||||
/* close commented backslash hack */
|
|
@ -0,0 +1,96 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Firefox Fortress</title>
|
||||
<meta type="description" content="" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="firefox-accounts" content="supported" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/bower_components/normalize-css/normalize.css"
|
||||
type="text/css"
|
||||
/>
|
||||
<link rel="stylesheet" href="/css/main.css" type="text/css" />
|
||||
<script src="/bower_components/modernizr/modernizr.js"></script>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="splash">
|
||||
<header>
|
||||
<img
|
||||
src="/img/transparent-logo.png"
|
||||
id="splash-logo"
|
||||
alt="123done-logo"
|
||||
width="72"
|
||||
height="70"
|
||||
/>
|
||||
<h1>Firefox Fortress</h1>
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-subscribe"
|
||||
>Download Firefox Fortress</a>
|
||||
</header>
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="two-col clearfix">
|
||||
<div class="left-col">
|
||||
<h3>Download Firefox Fortress</h3>
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-subscribe"
|
||||
>Click Here</a>
|
||||
</div>
|
||||
<div class="right-col">
|
||||
<img src="/img/list@2x.png" alt="list-logo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="lists">
|
||||
<header id="header-main">
|
||||
<div class="container">
|
||||
<h1>
|
||||
<span class="logo"></span
|
||||
><span class="title"
|
||||
>Firefox Fortress <span class="pro-status">Pro!</span></span
|
||||
>
|
||||
</h1>
|
||||
<div id="subscriptionCTA">
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-subscribe"
|
||||
>Download Firefox Fortress</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="todo">
|
||||
<div class="container">
|
||||
<div class="preroll">
|
||||
<div id="subscriptionCTA">
|
||||
<span
|
||||
>Subscribe for pro! LINK TBD!
|
||||
productId=fortressProProduct&</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer id="footer-main">
|
||||
<div class="container">
|
||||
<div class="logo"></div>
|
||||
<p>
|
||||
<strong>Firefox Fortress</strong>
|
||||
<a href="/about.html">Learn more!</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
После Ширина: | Высота: | Размер: 1.1 KiB |
После Ширина: | Высота: | Размер: 147 B |
После Ширина: | Высота: | Размер: 164 B |
После Ширина: | Высота: | Размер: 3.0 KiB |
После Ширина: | Высота: | Размер: 6.5 KiB |
После Ширина: | Высота: | Размер: 2.5 KiB |
После Ширина: | Высота: | Размер: 873 B |
После Ширина: | Высота: | Размер: 1.8 KiB |
После Ширина: | Высота: | Размер: 1.5 KiB |
После Ширина: | Высота: | Размер: 80 KiB |
После Ширина: | Высота: | Размер: 3.3 KiB |
После Ширина: | Высота: | Размер: 7.2 KiB |
После Ширина: | Высота: | Размер: 7.2 KiB |
|
@ -0,0 +1,117 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Firefox Fortress</title>
|
||||
<meta type="description" content="" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="firefox-accounts" content="supported" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="/bower_components/normalize-css/normalize.css"
|
||||
type="text/css"
|
||||
/>
|
||||
<link rel="stylesheet" href="/css/main.css" type="text/css" />
|
||||
<script src="/bower_components/modernizr/modernizr.js"></script>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="splash">
|
||||
<header>
|
||||
<img
|
||||
src="/img/transparent-logo.png"
|
||||
id="splash-logo"
|
||||
alt="123done-logo"
|
||||
width="72"
|
||||
height="70"
|
||||
/>
|
||||
<h1>Firefox Fortress</h1>
|
||||
<a
|
||||
href="//127.0.0.1:3030/subscriptions/products/fortressProProduct"
|
||||
class="btn btn-subscribe"
|
||||
>Subscribe for Pro</a>
|
||||
</header>
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="two-col clearfix">
|
||||
<div class="left-col">
|
||||
<h3>Protect your data</h3>
|
||||
<p>
|
||||
With Firefox Fortress, you can protect your data.
|
||||
</p>
|
||||
</div>
|
||||
<div class="right-col">
|
||||
<img src="/img/list@2x.png" alt="list-logo" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="two-col clearfix">
|
||||
<div class="left-col">
|
||||
<img src="/img/growth@2x.png" alt="growth-logo" />
|
||||
</div>
|
||||
<div class="right-col">
|
||||
<h3>Discover Yourself</h3>
|
||||
<p>
|
||||
Explore patterns in the way you work and improve your routines.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two-col clearfix">
|
||||
<div class="right-col">
|
||||
<h3>Do The Impossible</h3>
|
||||
<p>Achieve your dreams.</p>
|
||||
</div>
|
||||
<div class="right-col">
|
||||
<img src="/img/rocket@2x.png" alt="rocket-logo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="lists">
|
||||
<header id="header-main">
|
||||
<div class="container">
|
||||
<h1>
|
||||
<span class="logo"></span
|
||||
><span class="title"
|
||||
>Firefox Fortress <span class="pro-status">Pro!</span></span
|
||||
>
|
||||
</h1>
|
||||
<div id="subscriptionCTA">
|
||||
<a
|
||||
href="//127.0.0.1:3030/subscriptions/products/fortressProProduct"
|
||||
class="btn btn-subscribe"
|
||||
>Subscribe for Pro</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section class="todo">
|
||||
<div class="container">
|
||||
<div class="preroll">
|
||||
<div id="subscriptionCTA">
|
||||
<span
|
||||
>Subscribe for pro! LINK TBD!
|
||||
productId=fortressProProduct&</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer id="footer-main">
|
||||
<div class="container">
|
||||
<div class="logo"></div>
|
||||
<p>
|
||||
<strong>Firefox Fortress</strong>
|
||||
<a href="/about.html">Learn more!</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/fortress.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
q$(document).ready(function() {
|
||||
let paymentURL;
|
||||
switch (window.location.host) {
|
||||
case 'fortress-latest.dev.lcip.org':
|
||||
paymentURL =
|
||||
'https://latest.dev.lcip.org/subscriptions/products/plan_FUUOYlhpIhWtoo';
|
||||
break;
|
||||
default:
|
||||
paymentURL = '//127.0.0.1:3030/subscriptions/products/fortressProProduct';
|
||||
break;
|
||||
}
|
||||
$('.btn-subscribe').each(function(index) {
|
||||
$(this).attr('href', paymentURL);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
This is a Privacy Policy document for Firefox Fortress. If this were a
|
||||
real site, it would give you the policies which we adhere to as we
|
||||
deal with your personal, private data. Policies that you must accept
|
||||
in order to use the site.
|
||||
|
||||
But this is not a real site, it's a demonstration. So this document
|
||||
isn't really all that useful.
|
|
@ -0,0 +1,6 @@
|
|||
This is a Terms Of Service document for Firefox Fortress. If this were a
|
||||
real site, it would give you the terms of service that you must accept
|
||||
to use the site.
|
||||
|
||||
But this is not a real site, it's a demonstration. So this document
|
||||
isn't really all that useful.
|
|
@ -340,7 +340,7 @@ function createServer(db) {
|
|||
db.createAccountSubscription(
|
||||
req.params.id,
|
||||
req.params.subscriptionId,
|
||||
req.body.productName,
|
||||
req.body.productId,
|
||||
req.body.createdAt
|
||||
)
|
||||
)
|
||||
|
@ -364,7 +364,10 @@ function createServer(db) {
|
|||
api.post(
|
||||
'/account/:uid/subscriptions/:subscriptionId/reactivate',
|
||||
op(req =>
|
||||
db.reactivateAccountSubscription(req.params.uid, req.params.subscriptionId)
|
||||
db.reactivateAccountSubscription(
|
||||
req.params.uid,
|
||||
req.params.subscriptionId
|
||||
)
|
||||
)
|
||||
);
|
||||
api.get(
|
||||
|
|
|
@ -4238,6 +4238,8 @@ module.exports = function(config, DB) {
|
|||
});
|
||||
|
||||
it('should create a new subscription for account', async () => {
|
||||
await P.delay(20);
|
||||
|
||||
const result = await db.createAccountSubscription(
|
||||
account.uid,
|
||||
subscriptionIds[0],
|
||||
|
@ -4245,6 +4247,12 @@ module.exports = function(config, DB) {
|
|||
Date.now()
|
||||
);
|
||||
assert.deepEqual(result, {});
|
||||
|
||||
const updatedAccount = await db.account(account.uid);
|
||||
assert.isAbove(
|
||||
updatedAccount.profileChangedAt,
|
||||
updatedAccount.createdAt
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail to create for unknown account', async () => {
|
||||
|
@ -4339,12 +4347,14 @@ module.exports = function(config, DB) {
|
|||
new Set([subscriptionIds[3], subscriptionIds[4], subscriptionIds[5]])
|
||||
);
|
||||
assert.deepEqual(
|
||||
pickSet(result, 'productName'),
|
||||
pickSet(result, 'productId'),
|
||||
new Set(['prod4', 'prod5', 'prod6'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should support deleting a subscription', async () => {
|
||||
await P.delay(20);
|
||||
|
||||
await db.createAccountSubscription(
|
||||
account.uid,
|
||||
subscriptionIds[6],
|
||||
|
@ -4373,9 +4383,15 @@ module.exports = function(config, DB) {
|
|||
new Set([subscriptionIds[6], subscriptionIds[8]])
|
||||
);
|
||||
assert.deepEqual(
|
||||
pickSet(result, 'productName'),
|
||||
pickSet(result, 'productId'),
|
||||
new Set(['prod4', 'prod6'])
|
||||
);
|
||||
|
||||
const updatedAccount = await db.account(account.uid);
|
||||
assert.isAbove(
|
||||
updatedAccount.profileChangedAt,
|
||||
updatedAccount.createdAt
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw an error when subscription deletion is attempted for a non-existent subscription', async () => {
|
||||
|
@ -4412,7 +4428,7 @@ module.exports = function(config, DB) {
|
|||
])
|
||||
);
|
||||
assert.deepEqual(
|
||||
pickSet(result, 'productName'),
|
||||
pickSet(result, 'productId'),
|
||||
new Set(['prod4', 'prod5', 'prod6'])
|
||||
);
|
||||
});
|
||||
|
@ -4448,12 +4464,14 @@ module.exports = function(config, DB) {
|
|||
new Set([subscriptionIds[15], subscriptionIds[17]])
|
||||
);
|
||||
assert.deepEqual(
|
||||
pickSet(result, 'productName'),
|
||||
pickSet(result, 'productId'),
|
||||
new Set(['prod4', 'prod6'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should cancel subscriptions', async () => {
|
||||
await P.delay(20);
|
||||
|
||||
await db.createAccountSubscription(
|
||||
account.uid,
|
||||
subscriptionIds[18],
|
||||
|
@ -4485,6 +4503,12 @@ module.exports = function(config, DB) {
|
|||
pickSet(subscriptions, 'cancelledAt'),
|
||||
new Set([null, cancelledAt])
|
||||
);
|
||||
|
||||
const updatedAccount = await db.account(account.uid);
|
||||
assert.isAbove(
|
||||
updatedAccount.profileChangedAt,
|
||||
updatedAccount.createdAt
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail to cancel a non-existent subscription', async () => {
|
||||
|
@ -4526,6 +4550,8 @@ module.exports = function(config, DB) {
|
|||
});
|
||||
|
||||
it('should reactivate subscriptions', async () => {
|
||||
await P.delay(20);
|
||||
|
||||
await db.createAccountSubscription(
|
||||
account.uid,
|
||||
subscriptionIds[22],
|
||||
|
@ -4553,6 +4579,12 @@ module.exports = function(config, DB) {
|
|||
pickSet(subscriptions, 'cancelledAt'),
|
||||
new Set([null])
|
||||
);
|
||||
|
||||
const updatedAccount = await db.account(account.uid);
|
||||
assert.isAbove(
|
||||
updatedAccount.profileChangedAt,
|
||||
updatedAccount.createdAt
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail to reactivate a non-existent subscription', async () => {
|
||||
|
@ -4633,7 +4665,7 @@ module.exports = function(config, DB) {
|
|||
account.uid,
|
||||
subscriptionIds[9]
|
||||
);
|
||||
assert.equal(result.productName, 'prod7');
|
||||
assert.equal(result.productId, 'prod7');
|
||||
});
|
||||
|
||||
it('should fail to fetch a subscription that does not exist', async () => {
|
||||
|
|
|
@ -2961,7 +2961,7 @@ module.exports = function(cfg, makeServer) {
|
|||
it('should create a new subscription', async () => {
|
||||
const result = await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[0]}`,
|
||||
{ productName: prods[0], createdAt: Date.now() }
|
||||
{ productId: prods[0], createdAt: Date.now() }
|
||||
);
|
||||
respOkEmpty(result);
|
||||
});
|
||||
|
@ -2969,18 +2969,18 @@ module.exports = function(cfg, makeServer) {
|
|||
it('should get one subscription', async () => {
|
||||
const result = await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[1]}`,
|
||||
{ productName: prods[2], createdAt: Date.now() }
|
||||
{ productId: prods[2], createdAt: Date.now() }
|
||||
);
|
||||
respOkEmpty(result);
|
||||
|
||||
const {
|
||||
obj: { subscriptionId, productName },
|
||||
obj: { subscriptionId, productId },
|
||||
} = await client.getThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[1]}`
|
||||
);
|
||||
|
||||
assert.equal(subscriptionId, subs[1]);
|
||||
assert.equal(productName, prods[2]);
|
||||
assert.equal(productId, prods[2]);
|
||||
});
|
||||
|
||||
const pick = (list, name) => list.map(x => x[name]);
|
||||
|
@ -2988,15 +2988,15 @@ module.exports = function(cfg, makeServer) {
|
|||
it('should list subscriptions', async () => {
|
||||
await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[2]}`,
|
||||
{ productName: prods[3], createdAt: now - 30 }
|
||||
{ productId: prods[3], createdAt: now - 30 }
|
||||
);
|
||||
await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[3]}`,
|
||||
{ productName: prods[4], createdAt: now - 20 }
|
||||
{ productId: prods[4], createdAt: now - 20 }
|
||||
);
|
||||
await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[4]}`,
|
||||
{ productName: prods[5], createdAt: now - 10 }
|
||||
{ productId: prods[5], createdAt: now - 10 }
|
||||
);
|
||||
|
||||
const { obj } = await client.getThen(
|
||||
|
@ -3009,7 +3009,7 @@ module.exports = function(cfg, makeServer) {
|
|||
subs[3],
|
||||
subs[4],
|
||||
]);
|
||||
assert.deepEqual(pick(obj, 'productName'), [
|
||||
assert.deepEqual(pick(obj, 'productId'), [
|
||||
prods[3],
|
||||
prods[4],
|
||||
prods[5],
|
||||
|
@ -3020,15 +3020,15 @@ module.exports = function(cfg, makeServer) {
|
|||
it('should support deleting a subscription', async () => {
|
||||
await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[5]}`,
|
||||
{ productName: prods[6], createdAt: now - 30 }
|
||||
{ productId: prods[6], createdAt: now - 30 }
|
||||
);
|
||||
await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[6]}`,
|
||||
{ productName: prods[7], createdAt: now - 20 }
|
||||
{ productId: prods[7], createdAt: now - 20 }
|
||||
);
|
||||
await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[7]}`,
|
||||
{ productName: prods[8], createdAt: now - 10 }
|
||||
{ productId: prods[8], createdAt: now - 10 }
|
||||
);
|
||||
await client.delThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[6]}`
|
||||
|
@ -3040,13 +3040,13 @@ module.exports = function(cfg, makeServer) {
|
|||
|
||||
assert.lengthOf(obj, 2);
|
||||
assert.deepEqual(pick(obj, 'subscriptionId'), [subs[5], subs[7]]);
|
||||
assert.deepEqual(pick(obj, 'productName'), [prods[6], prods[8]]);
|
||||
assert.deepEqual(pick(obj, 'productId'), [prods[6], prods[8]]);
|
||||
});
|
||||
|
||||
it('should cancel and reactivate subscriptions', async () => {
|
||||
await client.putThen(
|
||||
`/account/${user.accountId}/subscriptions/${subs[8]}`,
|
||||
{ productName: prods[6], createdAt: now - 30 }
|
||||
{ productId: prods[6], createdAt: now - 30 }
|
||||
);
|
||||
const cancelledAt = Date.now();
|
||||
await client.postThen(
|
||||
|
|
|
@ -2346,7 +2346,7 @@ curl \
|
|||
-X PUT \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"productName" : "exampleProduct1",
|
||||
"productId" : "exampleProduct1",
|
||||
"createdAt" : 1424832691282
|
||||
}' \
|
||||
http://localhost:8000/account/6044486dd15b42e08b1fb9167415b9ac/subscriptions/sub8675309
|
||||
|
@ -2360,7 +2360,7 @@ curl \
|
|||
- `uid` : hex
|
||||
- `subscriptionId` : string255
|
||||
- Params:
|
||||
- `productName`: Name of the subscribed product from the upstream payment system
|
||||
- `productId`: Name of the subscribed product from the upstream payment system
|
||||
- `createdAt`: Date of subscription creation
|
||||
|
||||
### Response
|
||||
|
@ -2415,19 +2415,19 @@ Content-Length: 2
|
|||
{
|
||||
"uid": 6044486dd15b42e08b1fb9167415b9ac,
|
||||
"subscriptionId": "sub8675309",
|
||||
"productName": "exampleProduct1",
|
||||
"productId": "exampleProduct1",
|
||||
"createdAt": 1424832691282
|
||||
},
|
||||
{
|
||||
"uid": 6044486dd15b42e08b1fb9167415b9ac,
|
||||
"subscriptionId": "sub999",
|
||||
"productName": "exampleProduct2",
|
||||
"productId": "exampleProduct2",
|
||||
"createdAt": 1424832691282
|
||||
},
|
||||
{
|
||||
"uid": 6044486dd15b42e08b1fb9167415b9ac,
|
||||
"subscriptionId": "sub987",
|
||||
"productName": "exampleProduct3",
|
||||
"productId": "exampleProduct3",
|
||||
"createdAt": 1424832691282
|
||||
}
|
||||
]
|
||||
|
@ -2438,7 +2438,7 @@ Content-Length: 2
|
|||
- Content-Type : `application/json`
|
||||
- Body : `[{}]`
|
||||
- `subscriptionId`: ID for the subscription from the upstream payment system
|
||||
- `productName`: Name of the subscribed product from the upstream payment system
|
||||
- `productId`: Name of the subscribed product from the upstream payment system
|
||||
- `createdAt`: Date of subscription creation
|
||||
- Status Code : `500 Internal Server Error`
|
||||
- Conditions: if something goes wrong on the server
|
||||
|
@ -2477,7 +2477,7 @@ Content-Length: 2
|
|||
{
|
||||
"uid": 6044486dd15b42e08b1fb9167415b9ac,
|
||||
"subscriptionId": "sub8675309",
|
||||
"productName": "exampleProduct1",
|
||||
"productId": "exampleProduct1",
|
||||
"createdAt": 1424832691282
|
||||
}
|
||||
|
||||
|
@ -2487,7 +2487,7 @@ Content-Length: 2
|
|||
- Content-Type : `application/json`
|
||||
- Body : `{}`
|
||||
- `subscriptionId`: ID for the subscription from the upstream payment system
|
||||
- `productName`: Name of the subscribed product from the upstream payment system
|
||||
- `productId`: Name of the subscribed product from the upstream payment system
|
||||
- `createdAt`: Date of subscription creation
|
||||
- Status Code : `500 Internal Server Error`
|
||||
- Conditions: if something goes wrong on the server
|
||||
|
|
|
@ -76,7 +76,7 @@ There are a number of methods that a DB storage backend should implement:
|
|||
- .deleteRecoveryKey(uid)
|
||||
- .recoveryKeyExists(uid)
|
||||
- Subscriptions
|
||||
- .createAccountSubscription(uid, subscriptionId, productName, createdAt)
|
||||
- .createAccountSubscription(uid, subscriptionId, productId, createdAt)
|
||||
- .fetchAccountSubscriptions(uid)
|
||||
- .getAccountSubscription(uid, subscriptionId)
|
||||
- .deleteAccountSubscription(uid, subscriptionId)
|
||||
|
@ -1039,7 +1039,7 @@ Returns:
|
|||
- Rejects with:
|
||||
- Any error from the underlying storage system (wrapped in `error.wrap()`)
|
||||
|
||||
## .createAccountSubscription(uid, subscriptionId, productName, createdAt)
|
||||
## .createAccountSubscription(uid, subscriptionId, productId, createdAt)
|
||||
|
||||
Create a product subscription for this user.
|
||||
|
||||
|
@ -1049,7 +1049,7 @@ Parameters:
|
|||
The uid of the owning account
|
||||
- `subscriptionId` (String):
|
||||
The subscription ID from the upstream payment system
|
||||
- `productName` (String):
|
||||
- `productId` (String):
|
||||
The name of the product granted by the subscription
|
||||
- `createdAt` (number):
|
||||
Creation timestamp for the subscription, milliseconds since the epoch
|
||||
|
@ -1076,7 +1076,7 @@ Returns:
|
|||
- An array of objects:
|
||||
- `uid`
|
||||
- `subscriptionId`
|
||||
- `productName`
|
||||
- `productId`
|
||||
- `createdAt`
|
||||
- Rejects with:
|
||||
- Any error from the underlying storage system (wrapped in `error.wrap()`)
|
||||
|
@ -1098,7 +1098,7 @@ Returns:
|
|||
- An object `{}`
|
||||
- `uid`
|
||||
- `subscriptionId`
|
||||
- `productName`
|
||||
- `productId`
|
||||
- `createdAt`
|
||||
- Rejects with:
|
||||
- Any error from the underlying storage system (wrapped in `error.wrap()`)
|
||||
|
|
|
@ -1607,14 +1607,14 @@ module.exports = function(log, error) {
|
|||
Memory.prototype.createAccountSubscription = async function(
|
||||
uid,
|
||||
subscriptionId,
|
||||
productName,
|
||||
productId,
|
||||
createdAt
|
||||
) {
|
||||
// Ensure user account exists
|
||||
uid = uid.toString('hex');
|
||||
await getAccountByUid(uid);
|
||||
const account = await getAccountByUid(uid);
|
||||
|
||||
const key = [uid, subscriptionId, productName].join('|');
|
||||
const key = [uid, subscriptionId, productId].join('|');
|
||||
if (accountSubscriptions[key]) {
|
||||
throw error.duplicate();
|
||||
}
|
||||
|
@ -1627,10 +1627,11 @@ module.exports = function(log, error) {
|
|||
accountSubscriptions[key] = {
|
||||
uid,
|
||||
subscriptionId,
|
||||
productName,
|
||||
productId,
|
||||
createdAt,
|
||||
cancelledAt: null,
|
||||
};
|
||||
account.profileChangedAt = Date.now();
|
||||
return {};
|
||||
};
|
||||
|
||||
|
@ -1667,13 +1668,14 @@ module.exports = function(log, error) {
|
|||
) {
|
||||
// Ensure user account exists
|
||||
uid = uid.toString('hex');
|
||||
await getAccountByUid(uid);
|
||||
const account = await getAccountByUid(uid);
|
||||
const toDelete = Object.entries(accountSubscriptions)
|
||||
.filter(
|
||||
([key, s]) => s.uid === uid && s.subscriptionId === subscriptionId
|
||||
)
|
||||
.map(([key, s]) => key);
|
||||
toDelete.forEach(key => delete accountSubscriptions[key]);
|
||||
account.profileChangedAt = Date.now();
|
||||
return {};
|
||||
};
|
||||
|
||||
|
@ -1685,7 +1687,7 @@ module.exports = function(log, error) {
|
|||
uid = uid.toString('hex');
|
||||
|
||||
// Ensure user account exists
|
||||
await getAccountByUid(uid);
|
||||
const account = await getAccountByUid(uid);
|
||||
|
||||
const cancelled = Object.values(accountSubscriptions).some(subscription => {
|
||||
if (
|
||||
|
@ -1704,6 +1706,8 @@ module.exports = function(log, error) {
|
|||
throw error.notFound();
|
||||
}
|
||||
|
||||
account.profileChangedAt = Date.now();
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
|
@ -1714,7 +1718,7 @@ module.exports = function(log, error) {
|
|||
uid = uid.toString('hex');
|
||||
|
||||
// Ensure user account exists
|
||||
await getAccountByUid(uid);
|
||||
const account = await getAccountByUid(uid);
|
||||
|
||||
const reactivated = Object.values(accountSubscriptions).some(
|
||||
subscription => {
|
||||
|
@ -1735,6 +1739,8 @@ module.exports = function(log, error) {
|
|||
throw error.notFound();
|
||||
}
|
||||
|
||||
account.profileChangedAt = Date.now();
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
|
|
|
@ -1662,17 +1662,17 @@ module.exports = function(log, error) {
|
|||
};
|
||||
|
||||
const CREATE_ACCOUNT_SUBSCRIPTION =
|
||||
'CALL createAccountSubscription_1(?,?,?,?)';
|
||||
'CALL createAccountSubscription_3(?,?,?,?)';
|
||||
MySql.prototype.createAccountSubscription = function(
|
||||
uid,
|
||||
subscriptionId,
|
||||
productName,
|
||||
productId,
|
||||
createdAt
|
||||
) {
|
||||
return this.write(CREATE_ACCOUNT_SUBSCRIPTION, [
|
||||
uid,
|
||||
subscriptionId,
|
||||
productName,
|
||||
productId,
|
||||
createdAt,
|
||||
]).then(
|
||||
result => ({}),
|
||||
|
@ -1685,7 +1685,7 @@ module.exports = function(log, error) {
|
|||
);
|
||||
};
|
||||
|
||||
const GET_ACCOUNT_SUBSCRIPTION = 'CALL getAccountSubscription_1(?,?)';
|
||||
const GET_ACCOUNT_SUBSCRIPTION = 'CALL getAccountSubscription_2(?,?)';
|
||||
MySql.prototype.getAccountSubscription = function(uid, subscriptionId) {
|
||||
return this.readFirstResult(GET_ACCOUNT_SUBSCRIPTION, [
|
||||
uid,
|
||||
|
@ -1698,55 +1698,57 @@ module.exports = function(log, error) {
|
|||
// future, you must note this change in the deployment notes so the new
|
||||
// versioned name is granted the execute privilege, or
|
||||
// `fxa-support-panel` will break.
|
||||
const FETCH_ACCOUNT_SUBSCRIPTIONS = 'CALL fetchAccountSubscriptions_2(?)';
|
||||
const FETCH_ACCOUNT_SUBSCRIPTIONS = 'CALL fetchAccountSubscriptions_3(?)';
|
||||
MySql.prototype.fetchAccountSubscriptions = function(uid) {
|
||||
return this.readAllResults(FETCH_ACCOUNT_SUBSCRIPTIONS, [uid]);
|
||||
};
|
||||
|
||||
const DELETE_ACCOUNT_SUBSCRIPTION = 'CALL deleteAccountSubscription_1(?,?)';
|
||||
const DELETE_ACCOUNT_SUBSCRIPTION = 'CALL deleteAccountSubscription_2(?,?)';
|
||||
MySql.prototype.deleteAccountSubscription = function(uid, subscriptionId) {
|
||||
return this.write(DELETE_ACCOUNT_SUBSCRIPTION, [uid, subscriptionId]).then(
|
||||
result => ({})
|
||||
);
|
||||
};
|
||||
|
||||
const CANCEL_ACCOUNT_SUBSCRIPTION = 'CALL cancelAccountSubscription_1(?,?,?)';
|
||||
const CANCEL_ACCOUNT_SUBSCRIPTION = 'CALL cancelAccountSubscription_2(?,?,?)';
|
||||
MySql.prototype.cancelAccountSubscription = async function(
|
||||
uid,
|
||||
subscriptionId,
|
||||
cancelledAt
|
||||
) {
|
||||
const result = await this.read(CANCEL_ACCOUNT_SUBSCRIPTION, [
|
||||
return this.write(CANCEL_ACCOUNT_SUBSCRIPTION, [
|
||||
uid,
|
||||
subscriptionId,
|
||||
cancelledAt,
|
||||
]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
log.error('MySql.cancelAccountSubscription.notUpdated', { result });
|
||||
throw error.notFound();
|
||||
}
|
||||
|
||||
return {};
|
||||
]).then(
|
||||
result => ({}),
|
||||
err => {
|
||||
if (err.errno === ER_SIGNAL_NOT_FOUND) {
|
||||
throw error.notFound();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const REACTIVATE_ACCOUNT_SUBSCRIPTION =
|
||||
'CALL reactivateAccountSubscription_1(?,?)';
|
||||
'CALL reactivateAccountSubscription_2(?,?)';
|
||||
MySql.prototype.reactivateAccountSubscription = async function(
|
||||
uid,
|
||||
subscriptionId
|
||||
) {
|
||||
const result = await this.read(REACTIVATE_ACCOUNT_SUBSCRIPTION, [
|
||||
return this.write(REACTIVATE_ACCOUNT_SUBSCRIPTION, [
|
||||
uid,
|
||||
subscriptionId,
|
||||
]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
log.error('MySql.reactivateAccountSubscription.notUpdated', { result });
|
||||
throw error.notFound();
|
||||
}
|
||||
|
||||
return {};
|
||||
]).then(
|
||||
result => ({}),
|
||||
err => {
|
||||
if (err.errno === ER_SIGNAL_NOT_FOUND) {
|
||||
throw error.notFound();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return MySql;
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
// The expected patch level of the database. Update if you add a new
|
||||
// patch in the ./schema/ directory.
|
||||
|
||||
module.exports.level = 102;
|
||||
module.exports.level = 104;
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
SET NAMES utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
CALL assertPatchLevel('102');
|
||||
|
||||
-- Add column productId to the accountSubscriptions table.
|
||||
-- This is the first part of a two-step rename. A subsequent
|
||||
-- migration will set the value of productId to productName
|
||||
-- and then remove the productName column.
|
||||
ALTER TABLE accountSubscriptions
|
||||
ADD COLUMN productId VARCHAR(191),
|
||||
ADD UNIQUE INDEX UQ_accountSubscriptions_uid_productId_subscriptionId(uid, productId, subscriptionId),
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
CREATE PROCEDURE `createAccountSubscription_2` (
|
||||
IN inUid BINARY(16),
|
||||
IN inSubscriptionId VARCHAR(191),
|
||||
IN inProductId VARCHAR(191),
|
||||
IN inCreatedAt BIGINT SIGNED
|
||||
)
|
||||
BEGIN
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
SET @accountCount = 0;
|
||||
|
||||
-- Signal error if no user found
|
||||
SELECT COUNT(*) INTO @accountCount FROM accounts WHERE uid = inUid;
|
||||
IF @accountCount = 0 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1643, MESSAGE_TEXT = 'Can not create subscription for unknown user.';
|
||||
END IF;
|
||||
|
||||
INSERT INTO accountSubscriptions(
|
||||
uid,
|
||||
subscriptionId,
|
||||
productId,
|
||||
createdAt
|
||||
)
|
||||
VALUES (
|
||||
inUid,
|
||||
inSubscriptionId,
|
||||
inProductId,
|
||||
inCreatedAt
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
CREATE PROCEDURE `getAccountSubscription_2` (
|
||||
IN uidArg BINARY(16),
|
||||
IN subscriptionIdArg VARCHAR(191)
|
||||
)
|
||||
BEGIN
|
||||
SELECT uid, subscriptionId, COALESCE(productId, productName) AS productId, createdAt, cancelledAt
|
||||
FROM accountSubscriptions
|
||||
WHERE uid = uidArg
|
||||
AND subscriptionId = subscriptionIdArg;
|
||||
END;
|
||||
|
||||
CREATE PROCEDURE `fetchAccountSubscriptions_3` (
|
||||
IN uidArg BINARY(16)
|
||||
)
|
||||
BEGIN
|
||||
SELECT uid, subscriptionId, COALESCE(productId, productName) AS productId, createdAt, cancelledAt
|
||||
FROM accountSubscriptions
|
||||
WHERE uid = uidArg
|
||||
ORDER BY createdAt ASC;
|
||||
END;
|
||||
|
||||
UPDATE dbMetadata SET value = '103' WHERE name = 'schema-patch-level';
|
|
@ -0,0 +1,12 @@
|
|||
-- SET NAMES utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
-- DROP PROCEDURE `fetchAccountSubscriptions_3`;
|
||||
-- DROP PROCEDURE `getAccountSubscription_2`;
|
||||
-- DROP PROCEDURE `createAccountSubscription_2`;
|
||||
|
||||
-- ALTER TABLE `accountSubscriptions`
|
||||
-- DROP INDEX UQ_accountSubscriptions_uid_productId_subscriptionId,
|
||||
-- DROP COLUMN productId,
|
||||
-- ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '102' WHERE name = 'schema-patch-level';
|
|
@ -0,0 +1,135 @@
|
|||
SET NAMES utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
CALL assertPatchLevel('103');
|
||||
|
||||
CREATE PROCEDURE `createAccountSubscription_3` (
|
||||
IN inUid BINARY(16),
|
||||
IN inSubscriptionId VARCHAR(191),
|
||||
IN inProductId VARCHAR(191),
|
||||
IN inCreatedAt BIGINT SIGNED
|
||||
)
|
||||
BEGIN
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
SET @accountCount = 0;
|
||||
|
||||
-- Signal error if no user found
|
||||
SELECT COUNT(*) INTO @accountCount FROM accounts WHERE uid = inUid;
|
||||
IF @accountCount = 0 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1643, MESSAGE_TEXT = 'Can not create subscription for unknown user.';
|
||||
END IF;
|
||||
|
||||
INSERT INTO accountSubscriptions(
|
||||
uid,
|
||||
subscriptionId,
|
||||
productId,
|
||||
createdAt
|
||||
)
|
||||
VALUES (
|
||||
inUid,
|
||||
inSubscriptionId,
|
||||
inProductId,
|
||||
inCreatedAt
|
||||
);
|
||||
|
||||
UPDATE accounts SET profileChangedAt = (UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE uid = inUid;
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
CREATE PROCEDURE `deleteAccountSubscription_2` (
|
||||
IN inUid BINARY(16),
|
||||
IN inSubscriptionId VARCHAR(191)
|
||||
)
|
||||
BEGIN
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
DELETE FROM accountSubscriptions
|
||||
WHERE
|
||||
uid = inUid
|
||||
AND
|
||||
subscriptionId = inSubscriptionId;
|
||||
|
||||
UPDATE accounts SET profileChangedAt = (UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE uid = inUid;
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
CREATE PROCEDURE `cancelAccountSubscription_2` (
|
||||
IN uidArg BINARY(16),
|
||||
IN subscriptionIdArg VARCHAR(191),
|
||||
IN cancelledAtArg BIGINT UNSIGNED
|
||||
)
|
||||
BEGIN
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
SET @cancelledCount = 0;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
UPDATE accountSubscriptions
|
||||
SET cancelledAt = cancelledAtArg
|
||||
WHERE uid = uidArg
|
||||
AND subscriptionId = subscriptionIdArg
|
||||
AND cancelledAt IS NULL;
|
||||
|
||||
SELECT ROW_COUNT() INTO @cancelledCount;
|
||||
|
||||
IF @cancelledCount = 0 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1643, MESSAGE_TEXT = 'No subscriptions were cancelled.';
|
||||
END IF;
|
||||
|
||||
UPDATE accounts SET profileChangedAt = (UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE uid = uidArg;
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
CREATE PROCEDURE `reactivateAccountSubscription_2` (
|
||||
IN uidArg BINARY(16),
|
||||
IN subscriptionIdArg VARCHAR(191)
|
||||
)
|
||||
BEGIN
|
||||
DECLARE EXIT HANDLER FOR SQLEXCEPTION
|
||||
BEGIN
|
||||
ROLLBACK;
|
||||
RESIGNAL;
|
||||
END;
|
||||
|
||||
SET @reactivatedCount = 0;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
UPDATE accountSubscriptions
|
||||
SET cancelledAt = NULL
|
||||
WHERE uid = uidArg
|
||||
AND subscriptionId = subscriptionIdArg
|
||||
AND cancelledAt IS NOT NULL;
|
||||
|
||||
SELECT ROW_COUNT() INTO @reactivatedCount;
|
||||
|
||||
IF @reactivatedCount = 0 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MYSQL_ERRNO = 1643, MESSAGE_TEXT = 'No subscriptions were reactivated.';
|
||||
END IF;
|
||||
|
||||
UPDATE accounts SET profileChangedAt = (UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE uid = uidArg;
|
||||
|
||||
COMMIT;
|
||||
END;
|
||||
|
||||
UPDATE dbMetadata SET value = '104' WHERE name = 'schema-patch-level';
|
|
@ -0,0 +1,8 @@
|
|||
-- SET NAMES utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
-- DROP PROCEDURE `createAccountSubscription_3`;
|
||||
-- DROP PROCEDURE `deleteAccountSubscription_2`;
|
||||
-- DROP PROCEDURE `cancelAccountSubscription_2`;
|
||||
-- DROP PROCEDURE `reactivateAccountSubscription_2`;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '103' WHERE name = 'schema-patch-level';
|
|
@ -7,12 +7,6 @@
|
|||
// NEW_RELIC_LICENSE_KEY.
|
||||
|
||||
function maybeRequireNewRelic() {
|
||||
var env = process.env;
|
||||
|
||||
if (env.NEW_RELIC_APP_NAME && env.NEW_RELIC_LICENSE_KEY) {
|
||||
return require('newrelic');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -159,37 +159,6 @@
|
|||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@newrelic/koa": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@newrelic/koa/-/koa-1.0.8.tgz",
|
||||
"integrity": "sha512-kY//FlLQkGdUIKEeGJlyY3dJRU63EG77YIa48ACMGZxQbWRd3WZMikyft33f8XScTq6WpCDo9xa0viNo8zeYkg==",
|
||||
"requires": {
|
||||
"methods": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"@newrelic/native-metrics": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@newrelic/native-metrics/-/native-metrics-3.1.2.tgz",
|
||||
"integrity": "sha512-JjUmPrp2LEEkhVtelICme5p7sHHpfpu2Wjk5/L1D3Zvt01v4mCsrL2XaIMBmHgg3T2ZbqMiqWZCn2LtGZ6nklA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "^2.10.0",
|
||||
"semver": "^5.5.1"
|
||||
}
|
||||
},
|
||||
"@newrelic/superagent": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@newrelic/superagent/-/superagent-1.0.3.tgz",
|
||||
"integrity": "sha512-lJbsqKa79qPLbHZsbiRaXl1jfzaXAN7zqqnLRqBY+zI/O5zcfyNngTmdi+9y+qIUq7xHYNaLsAxCXerrsoINKg==",
|
||||
"requires": {
|
||||
"methods": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"@tyriar/fibonacci-heap": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tyriar/fibonacci-heap/-/fibonacci-heap-2.0.9.tgz",
|
||||
"integrity": "sha512-bYuSNomfn4hu2tPiDN+JZtnzCpSpbJ/PNeulmocDy3xN2X5OkJL65zo6rPZp65cPPhLF9vfT/dgE+RtFRCSxOA=="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
|
@ -208,14 +177,6 @@
|
|||
"integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
|
||||
"dev": true
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
|
||||
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
|
||||
"requires": {
|
||||
"es6-promisify": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
|
||||
|
@ -406,11 +367,6 @@
|
|||
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
|
||||
"dev": true
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||
},
|
||||
"bunyan": {
|
||||
"version": "1.8.12",
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
|
||||
|
@ -565,17 +521,6 @@
|
|||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"concat-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^2.2.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"convict": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/convict/-/convict-4.4.0.tgz",
|
||||
|
@ -678,14 +623,6 @@
|
|||
"resolved": "https://registry.npmjs.org/dbug/-/dbug-0.4.2.tgz",
|
||||
"integrity": "sha1-MrSzEF6IYQQ6b5rHVdgOVC02WzE="
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
|
@ -818,19 +755,6 @@
|
|||
"is-symbol": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz",
|
||||
"integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q=="
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
|
||||
"requires": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"escape-regexp-component": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/escape-regexp-component/-/escape-regexp-component-1.0.2.tgz",
|
||||
|
@ -1648,15 +1572,6 @@
|
|||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
|
||||
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
|
||||
"requires": {
|
||||
"agent-base": "^4.1.0",
|
||||
"debug": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
|
@ -2124,11 +2039,6 @@
|
|||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
|
||||
"dev": true
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
|
@ -2268,7 +2178,8 @@
|
|||
"ms": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||
"dev": true
|
||||
},
|
||||
"multimatch": {
|
||||
"version": "2.1.0",
|
||||
|
@ -2352,33 +2263,6 @@
|
|||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"newrelic": {
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/newrelic/-/newrelic-4.10.0.tgz",
|
||||
"integrity": "sha512-ih/tdO5jMcZ7oYiL7xtES4RBPp7EzBg5Shj8VWBmJ2lOxjWNFwTJ7EqQ7Q0U1xK1zkuZ6Gp5q37Un/N3+QAMFw==",
|
||||
"requires": {
|
||||
"@newrelic/koa": "^1.0.0",
|
||||
"@newrelic/native-metrics": "^3.0.0",
|
||||
"@newrelic/superagent": "^1.0.0",
|
||||
"@tyriar/fibonacci-heap": "^2.0.7",
|
||||
"async": "^2.1.4",
|
||||
"concat-stream": "^1.5.0",
|
||||
"https-proxy-agent": "^2.2.1",
|
||||
"json-stringify-safe": "^5.0.0",
|
||||
"readable-stream": "^2.1.4",
|
||||
"semver": "^5.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz",
|
||||
"integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
|
@ -4503,11 +4387,6 @@
|
|||
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||
"dev": true
|
||||
},
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
},
|
||||
"underscore.string": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz",
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
"mozlog": "2.2.0",
|
||||
"mysql": "2.16.0",
|
||||
"mysql-patcher": "0.7.0",
|
||||
"newrelic": "4.10.0",
|
||||
"raven": "2.6.4",
|
||||
"request": "2.88.0",
|
||||
"restify": "7.2.2"
|
||||
|
|
|
@ -32,6 +32,7 @@ COPY ["fxa-shared", "../fxa-shared/"]
|
|||
WORKDIR /fxa-shared
|
||||
USER root
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ COPY ["fxa-shared", "../fxa-shared/"]
|
|||
WORKDIR /fxa-shared
|
||||
USER root
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
USER app
|
||||
|
||||
# Build final image by copying from builder
|
||||
|
|
|
@ -69,6 +69,15 @@
|
|||
"amount": 50,
|
||||
"currency": "usd"
|
||||
},
|
||||
{
|
||||
"plan_id": "fortressProMonthly",
|
||||
"plan_name": "Fortress Pro Monthly",
|
||||
"product_id": "fortressProProduct",
|
||||
"product_name": "Fortress Pro",
|
||||
"interval": "month",
|
||||
"amount": 50,
|
||||
"currency": "usd"
|
||||
},
|
||||
{
|
||||
"plan_id": "321doneProWeekly",
|
||||
"plan_name": "321done Pro Weekly",
|
||||
|
|
|
@ -216,5 +216,5 @@ so other services probably shouldn't use this.
|
|||
- `uid`: User id.
|
||||
- `subscriptionId`: Subscription id.
|
||||
- `isActive`: Boolean indicating whether the subscription is active.
|
||||
- `productName`: Product name.
|
||||
- `productId`: Product id.
|
||||
- `productCapabilities`: Array of product capabilities.
|
||||
|
|
|
@ -75,6 +75,7 @@ The currently-defined error responses are:
|
|||
- [POST /v1/key-data][key-data]
|
||||
- [POST /v1/authorized-clients][authorized-clients]
|
||||
- [POST /v1/authorized-clients/destroy][authorized-clients-destroy]
|
||||
- [POST /v1/introspect][introspect]
|
||||
- (**DEPRECATED**) [GET /v1/client-tokens][client-tokens]
|
||||
- (**DEPRECATED**) [DELETE /v1/client-tokens/:id][client-tokens-delete]
|
||||
|
||||
|
@ -709,6 +710,53 @@ curl -X POST \
|
|||
|
||||
A valid 200 response will return an empty JSON object.
|
||||
|
||||
### POST /v1/introspect
|
||||
|
||||
This endpoint returns the status of the token and meta-information about this token.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
- `token`: An OAuth token for the user.
|
||||
- `token_type_hint`: A literal string `"access_token"` or `"refresh_token"`
|
||||
|
||||
**Example:**
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
"https://oauth.accounts.firefox.com/v1/introspect" \
|
||||
-d '{"token":"5e00491407a01507bdc4002fd7b675fb4e7d039045a7e6755e4aed0d3e287c69"}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
A valid request will return a JSON response with these properties:
|
||||
|
||||
- `active`: Boolean indicator of weather the presented token is active.
|
||||
- `scope`: Optional. A space-seperated list of scopes associated with this token.
|
||||
- `client_id`: Optional. The hex id of the client whose token was passed.
|
||||
- `token_type`: A string representing the token type. It will be `"access_token"` or `"refresh_token"`.
|
||||
- `iat`: Optional. Integer time of token creation.
|
||||
- `sub`: Optional. The hex id of the user.
|
||||
- `jti`: Optional. The hex id of the token
|
||||
- `exp`: Optional. Integer time of token expiration.
|
||||
- `fxa-lastUsedAt`: Optional. Integer time when this token is last used.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"active": true,
|
||||
"scope": "profile https://identity.mozilla.com/account/subscriptions",
|
||||
"client_id": "59cceb6f8c32317c",
|
||||
"token_type": "access_token",
|
||||
"iat": 1566535888243,
|
||||
"sub": "913fe9395bb946b48c1521d7beb2cb24",
|
||||
"jti": "5ae05d8fe413a749e0f4eb3c495a1c526fb52c85ca5fde516df5dd77d41f7b5b",
|
||||
"exp": 1566537688243
|
||||
}
|
||||
```
|
||||
|
||||
### GET /v1/client-tokens
|
||||
|
||||
**DEPRECATED**: Please use [POST /v1/authorized-clients][authorized-clients] instead.
|
||||
|
@ -785,7 +833,7 @@ curl -X DELETE
|
|||
A valid 200 response will return an empty JSON object.
|
||||
|
||||
[client]: #get-v1clientid
|
||||
[register]: #post-v1clientregister
|
||||
[register]: #post-v1client
|
||||
[clients]: #get-v1clients
|
||||
[client-update]: #post-v1clientid
|
||||
[client-delete]: #delete-v1clientid
|
||||
|
@ -797,8 +845,9 @@ A valid 200 response will return an empty JSON object.
|
|||
[developer-activate]: #post-v1developeractivate
|
||||
[jwks]: #get-v1jwks
|
||||
[key-data]: #post-v1post-keydata
|
||||
[authorized-clients]: #post-v1authorized-clients
|
||||
[authorized-clients]: #get-v1authorized-clients
|
||||
[authorized-clients-destroy]: #post-v1authorized-clientsdestroy
|
||||
[introspect]: #post-v1introspect
|
||||
[client-tokens]: #get-v1client-tokens
|
||||
[client-tokens-delete]: #delete-v1client-tokensid
|
||||
[prompt-none]: https://github.com/mozilla/fxa/blob/master/packages/fxa-auth-server/fxa-oauth-server/docs/prompt-none.md
|
||||
|
|
|
@ -1523,12 +1523,12 @@ module.exports = (config, log, Token, UnblockCode = null) => {
|
|||
'db.createAccountSubscription'
|
||||
);
|
||||
DB.prototype.createAccountSubscription = function(data) {
|
||||
const { uid, subscriptionId, productName, createdAt } = data;
|
||||
const { uid, subscriptionId, productId, createdAt } = data;
|
||||
log.trace('DB.createAccountSubscription', data);
|
||||
return this.pool.put(
|
||||
SAFE_URLS.createAccountSubscription,
|
||||
{ uid, subscriptionId },
|
||||
{ productName, createdAt }
|
||||
{ productId, createdAt }
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1583,10 +1583,10 @@ module.exports = (config, log, Token, UnblockCode = null) => {
|
|||
);
|
||||
DB.prototype.reactivateAccountSubscription = function(uid, subscriptionId) {
|
||||
log.trace('DB.reactivateAccountSubscription', { uid, subscriptionId });
|
||||
return this.pool.post(
|
||||
SAFE_URLS.reactivateAccountSubscription,
|
||||
{ uid, subscriptionId },
|
||||
);
|
||||
return this.pool.post(SAFE_URLS.reactivateAccountSubscription, {
|
||||
uid,
|
||||
subscriptionId,
|
||||
});
|
||||
};
|
||||
|
||||
SAFE_URLS.fetchAccountSubscriptions = new SafeUrl(
|
||||
|
|
|
@ -9,12 +9,6 @@
|
|||
// NEW_RELIC_LICENSE_KEY.
|
||||
|
||||
function maybeRequireNewRelic() {
|
||||
const env = process.env;
|
||||
|
||||
if (env.NEW_RELIC_APP_NAME && env.NEW_RELIC_LICENSE_KEY) {
|
||||
return require('newrelic');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -247,10 +247,9 @@ module.exports = (
|
|||
throw new error.featureNotEnabled();
|
||||
}
|
||||
|
||||
return pushbox.retrieve(uid, deviceId, limit, index).then(resp => {
|
||||
log.info('commands.fetch', { resp: resp });
|
||||
return resp;
|
||||
});
|
||||
const response = await pushbox.retrieve(uid, deviceId, limit, index);
|
||||
log.info('commands.fetch', { response });
|
||||
return response;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -293,45 +292,38 @@ module.exports = (
|
|||
throw new error.featureNotEnabled();
|
||||
}
|
||||
|
||||
return customs
|
||||
.checkAuthenticated(request, uid, 'invokeDeviceCommand')
|
||||
.then(() => db.device(uid, target))
|
||||
.then(device => {
|
||||
if (!device.availableCommands.hasOwnProperty(command)) {
|
||||
throw error.unavailableDeviceCommand();
|
||||
}
|
||||
// 0 is perfectly acceptable TTL, hence the strict equality check.
|
||||
if (ttl === undefined && DEFAULT_COMMAND_TTL.has(command)) {
|
||||
ttl = DEFAULT_COMMAND_TTL.get(command);
|
||||
}
|
||||
const data = {
|
||||
command,
|
||||
payload,
|
||||
sender,
|
||||
};
|
||||
return pushbox
|
||||
.store(uid, device.id, data, ttl)
|
||||
.then(({ index }) => {
|
||||
const url = new URL(
|
||||
'v1/account/device/commands',
|
||||
config.publicUrl
|
||||
);
|
||||
url.searchParams.set('index', index);
|
||||
url.searchParams.set('limit', 1);
|
||||
return push.notifyCommandReceived(
|
||||
uid,
|
||||
device,
|
||||
command,
|
||||
sender,
|
||||
index,
|
||||
url.href,
|
||||
ttl
|
||||
);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return {};
|
||||
});
|
||||
const [, device] = await Promise.all([
|
||||
customs.checkAuthenticated(request, uid, 'invokeDeviceCommand'),
|
||||
db.device(uid, target),
|
||||
]);
|
||||
|
||||
if (!device.availableCommands.hasOwnProperty(command)) {
|
||||
throw error.unavailableDeviceCommand();
|
||||
}
|
||||
|
||||
// 0 is perfectly acceptable TTL, hence the strict equality check.
|
||||
if (ttl === undefined && DEFAULT_COMMAND_TTL.has(command)) {
|
||||
ttl = DEFAULT_COMMAND_TTL.get(command);
|
||||
}
|
||||
|
||||
const data = { command, payload, sender };
|
||||
const { index } = await pushbox.store(uid, device.id, data, ttl);
|
||||
|
||||
const url = new URL('v1/account/device/commands', config.publicUrl);
|
||||
url.searchParams.set('index', index);
|
||||
url.searchParams.set('limit', 1);
|
||||
|
||||
await push.notifyCommandReceived(
|
||||
uid,
|
||||
device,
|
||||
command,
|
||||
sender,
|
||||
index,
|
||||
url.href,
|
||||
ttl
|
||||
);
|
||||
|
||||
return {};
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -430,66 +422,62 @@ module.exports = (
|
|||
pushOptions.TTL = body.TTL;
|
||||
}
|
||||
|
||||
return customs
|
||||
.checkAuthenticated(request, uid, endpointAction)
|
||||
.then(() => request.app.devices)
|
||||
.then(devices => {
|
||||
if (body.to !== 'all') {
|
||||
const include = new Set(body.to);
|
||||
devices = devices.filter(device => include.has(device.id));
|
||||
let [, deviceArray] = await Promise.all([
|
||||
customs.checkAuthenticated(request, uid, endpointAction),
|
||||
request.app.devices,
|
||||
]);
|
||||
|
||||
if (devices.length === 0) {
|
||||
log.error('Account.devicesNotify', {
|
||||
uid: uid,
|
||||
error: 'devices empty',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (body.excluded) {
|
||||
const exclude = new Set(body.excluded);
|
||||
devices = devices.filter(device => !exclude.has(device.id));
|
||||
}
|
||||
if (body.to !== 'all') {
|
||||
const include = new Set(body.to);
|
||||
deviceArray = deviceArray.filter(device => include.has(device.id));
|
||||
|
||||
return push
|
||||
.sendPush(uid, devices, endpointAction, pushOptions)
|
||||
.catch(catchPushError);
|
||||
})
|
||||
.then(() => {
|
||||
// Emit a metrics event for when a user sends tabs between devices.
|
||||
// In the future we will aim to get this event directly from sync telemetry,
|
||||
// but we're doing it here for now as a quick way to get metrics on the feature.
|
||||
if (
|
||||
payload &&
|
||||
payload.command === 'sync:collection_changed' &&
|
||||
// Note that payload schema validation ensures that these properties exist.
|
||||
payload.data.collections.length === 1 &&
|
||||
payload.data.collections[0] === 'clients'
|
||||
) {
|
||||
let deviceId = undefined;
|
||||
if (deviceArray.length === 0) {
|
||||
log.error('Account.devicesNotify', {
|
||||
uid: uid,
|
||||
error: 'devices empty',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (body.excluded) {
|
||||
const exclude = new Set(body.excluded);
|
||||
deviceArray = deviceArray.filter(device => !exclude.has(device.id));
|
||||
}
|
||||
|
||||
if (sessionToken.deviceId) {
|
||||
deviceId = sessionToken.deviceId;
|
||||
}
|
||||
|
||||
return request.emitMetricsEvent('sync.sentTabToDevice', {
|
||||
device_id: deviceId,
|
||||
service: 'sync',
|
||||
uid: uid,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return {};
|
||||
});
|
||||
|
||||
function catchPushError(err) {
|
||||
try {
|
||||
await push.sendPush(uid, deviceArray, endpointAction, pushOptions);
|
||||
} catch (err) {
|
||||
// push may fail due to not found devices or a bad push action
|
||||
// log the error but still respond with a 200.
|
||||
// log the error but still respond with a 200
|
||||
log.error('Account.devicesNotify', {
|
||||
uid: uid,
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
|
||||
// Emit a metrics event for when a user sends tabs between devices.
|
||||
// In the future we will aim to get this event directly from sync telemetry,
|
||||
// but we're doing it here for now as a quick way to get metrics on the feature.
|
||||
if (
|
||||
payload &&
|
||||
payload.command === 'sync:collection_changed' &&
|
||||
// Note that payload schema validation ensures that these properties exist.
|
||||
payload.data.collections.length === 1 &&
|
||||
payload.data.collections[0] === 'clients'
|
||||
) {
|
||||
let deviceId;
|
||||
|
||||
if (sessionToken.deviceId) {
|
||||
deviceId = sessionToken.deviceId;
|
||||
}
|
||||
|
||||
await request.emitMetricsEvent('sync.sentTabToDevice', {
|
||||
device_id: deviceId,
|
||||
service: 'sync',
|
||||
uid: uid,
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -556,34 +544,36 @@ module.exports = (
|
|||
throw new error.featureNotEnabled();
|
||||
}
|
||||
|
||||
return request.app.devices.then(deviceArray => {
|
||||
return deviceArray.map(device => {
|
||||
const formattedDevice = {
|
||||
id: device.id,
|
||||
isCurrentDevice: !!(
|
||||
(credentials.id && credentials.id === device.sessionTokenId) ||
|
||||
(credentials.refreshTokenId &&
|
||||
credentials.refreshTokenId === device.refreshTokenId)
|
||||
),
|
||||
lastAccessTime: device.lastAccessTime,
|
||||
location: device.location,
|
||||
name: device.name || devices.synthesizeName(device),
|
||||
// For now we assume that all oauth clients that register a device record are mobile apps.
|
||||
// Ref https://github.com/mozilla/fxa/issues/449
|
||||
type:
|
||||
device.type ||
|
||||
device.uaDeviceType ||
|
||||
(device.refreshTokenId ? 'mobile' : 'desktop'),
|
||||
pushCallback: device.pushCallback,
|
||||
pushPublicKey: device.pushPublicKey,
|
||||
pushAuthKey: device.pushAuthKey,
|
||||
pushEndpointExpired: device.pushEndpointExpired,
|
||||
availableCommands: device.availableCommands,
|
||||
};
|
||||
clientUtils.formatTimestamps(formattedDevice, request);
|
||||
clientUtils.formatLocation(formattedDevice, request);
|
||||
return formattedDevice;
|
||||
});
|
||||
const deviceArray = await request.app.devices;
|
||||
|
||||
return deviceArray.map(device => {
|
||||
const formattedDevice = {
|
||||
id: device.id,
|
||||
isCurrentDevice: !!(
|
||||
(credentials.id && credentials.id === device.sessionTokenId) ||
|
||||
(credentials.refreshTokenId &&
|
||||
credentials.refreshTokenId === device.refreshTokenId)
|
||||
),
|
||||
lastAccessTime: device.lastAccessTime,
|
||||
location: device.location,
|
||||
name: device.name || devices.synthesizeName(device),
|
||||
// For now we assume that all oauth clients that register a device record are mobile apps.
|
||||
// Ref https://github.com/mozilla/fxa/issues/449
|
||||
type:
|
||||
device.type ||
|
||||
device.uaDeviceType ||
|
||||
(device.refreshTokenId ? 'mobile' : 'desktop'),
|
||||
pushCallback: device.pushCallback,
|
||||
pushPublicKey: device.pushPublicKey,
|
||||
pushAuthKey: device.pushAuthKey,
|
||||
pushEndpointExpired: device.pushEndpointExpired,
|
||||
availableCommands: device.availableCommands,
|
||||
};
|
||||
|
||||
clientUtils.formatTimestamps(formattedDevice, request);
|
||||
clientUtils.formatLocation(formattedDevice, request);
|
||||
|
||||
return formattedDevice;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -676,48 +666,50 @@ module.exports = (
|
|||
const sessionToken = request.auth.credentials;
|
||||
const uid = sessionToken.uid;
|
||||
|
||||
return db.sessions(uid).then(sessions => {
|
||||
return sessions.map(session => {
|
||||
const deviceId = session.deviceId;
|
||||
const isDevice = !!deviceId;
|
||||
const sessions = await db.sessions(uid);
|
||||
|
||||
let deviceName = session.deviceName;
|
||||
if (!deviceName) {
|
||||
deviceName = devices.synthesizeName(session);
|
||||
}
|
||||
return sessions.map(session => {
|
||||
const deviceId = session.deviceId;
|
||||
const isDevice = !!deviceId;
|
||||
|
||||
let userAgent;
|
||||
if (!session.uaBrowser) {
|
||||
userAgent = '';
|
||||
} else if (!session.uaBrowserVersion) {
|
||||
userAgent = session.uaBrowser;
|
||||
} else {
|
||||
const { uaBrowser: browser, uaBrowserVersion: version } = session;
|
||||
userAgent = `${browser} ${version.split('.')[0]}`;
|
||||
}
|
||||
let deviceName = session.deviceName;
|
||||
if (!deviceName) {
|
||||
deviceName = devices.synthesizeName(session);
|
||||
}
|
||||
|
||||
const formattedSession = {
|
||||
deviceId,
|
||||
deviceName,
|
||||
deviceType: session.uaDeviceType || 'desktop',
|
||||
deviceAvailableCommands: session.deviceAvailableCommands || null,
|
||||
deviceCallbackURL: session.deviceCallbackURL,
|
||||
deviceCallbackPublicKey: session.deviceCallbackPublicKey,
|
||||
deviceCallbackAuthKey: session.deviceCallbackAuthKey,
|
||||
deviceCallbackIsExpired: !!session.deviceCallbackIsExpired,
|
||||
id: session.id,
|
||||
isCurrentDevice: session.id === sessionToken.id,
|
||||
isDevice,
|
||||
lastAccessTime: session.lastAccessTime,
|
||||
location: session.location,
|
||||
createdTime: session.createdAt,
|
||||
os: session.uaOS,
|
||||
userAgent,
|
||||
};
|
||||
clientUtils.formatTimestamps(formattedSession, request);
|
||||
clientUtils.formatLocation(formattedSession, request);
|
||||
return formattedSession;
|
||||
});
|
||||
let userAgent;
|
||||
if (!session.uaBrowser) {
|
||||
userAgent = '';
|
||||
} else if (!session.uaBrowserVersion) {
|
||||
userAgent = session.uaBrowser;
|
||||
} else {
|
||||
const { uaBrowser: browser, uaBrowserVersion: version } = session;
|
||||
userAgent = `${browser} ${version.split('.')[0]}`;
|
||||
}
|
||||
|
||||
const formattedSession = {
|
||||
deviceId,
|
||||
deviceName,
|
||||
deviceType: session.uaDeviceType || 'desktop',
|
||||
deviceAvailableCommands: session.deviceAvailableCommands || null,
|
||||
deviceCallbackURL: session.deviceCallbackURL,
|
||||
deviceCallbackPublicKey: session.deviceCallbackPublicKey,
|
||||
deviceCallbackAuthKey: session.deviceCallbackAuthKey,
|
||||
deviceCallbackIsExpired: !!session.deviceCallbackIsExpired,
|
||||
id: session.id,
|
||||
isCurrentDevice: session.id === sessionToken.id,
|
||||
isDevice,
|
||||
lastAccessTime: session.lastAccessTime,
|
||||
location: session.location,
|
||||
createdTime: session.createdAt,
|
||||
os: session.uaOS,
|
||||
userAgent,
|
||||
};
|
||||
|
||||
clientUtils.formatTimestamps(formattedSession, request);
|
||||
clientUtils.formatLocation(formattedSession, request);
|
||||
|
||||
return formattedSession;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
const error = require('../error');
|
||||
const isA = require('joi');
|
||||
const P = require('../promise');
|
||||
const validators = require('./validators');
|
||||
|
||||
module.exports = (log, signer, db, domain, devices) => {
|
||||
|
@ -56,7 +55,7 @@ module.exports = (log, signer, db, domain, devices) => {
|
|||
const publicKey = request.payload.publicKey;
|
||||
const duration = request.payload.duration;
|
||||
const service = request.query.service;
|
||||
let deviceId, uid, certResult;
|
||||
let deviceId;
|
||||
if (request.headers['user-agent']) {
|
||||
const {
|
||||
browser: uaBrowser,
|
||||
|
@ -90,107 +89,97 @@ module.exports = (log, signer, db, domain, devices) => {
|
|||
throw error.unverifiedSession();
|
||||
}
|
||||
|
||||
return P.resolve()
|
||||
.then(() => {
|
||||
if (sessionToken.deviceId) {
|
||||
deviceId = sessionToken.deviceId;
|
||||
} else if (!service || service === 'sync') {
|
||||
// Synthesize a device record for Sync sessions that don't already have one.
|
||||
// Include the UA info so that we can synthesize a device name
|
||||
// for any push notifications.
|
||||
const deviceInfo = {
|
||||
uaBrowser: sessionToken.uaBrowser,
|
||||
uaBrowserVersion: sessionToken.uaBrowserVersion,
|
||||
uaOS: sessionToken.uaOS,
|
||||
uaOSVersion: sessionToken.uaOSVersion,
|
||||
};
|
||||
return devices
|
||||
.upsert(request, sessionToken, deviceInfo)
|
||||
.then(result => {
|
||||
deviceId = result.id;
|
||||
})
|
||||
.catch(err => {
|
||||
// There's a small chance that a device registration was performed
|
||||
// concurrently. If so, just use that device id.
|
||||
if (err.errno !== error.ERRNO.DEVICE_CONFLICT) {
|
||||
throw err;
|
||||
}
|
||||
deviceId = err.output.payload.deviceId;
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (publicKey.algorithm === 'RS') {
|
||||
if (!publicKey.n) {
|
||||
throw error.missingRequestParameter('n');
|
||||
}
|
||||
if (!publicKey.e) {
|
||||
throw error.missingRequestParameter('e');
|
||||
}
|
||||
} else {
|
||||
// DS
|
||||
if (!publicKey.y) {
|
||||
throw error.missingRequestParameter('y');
|
||||
}
|
||||
if (!publicKey.p) {
|
||||
throw error.missingRequestParameter('p');
|
||||
}
|
||||
if (!publicKey.q) {
|
||||
throw error.missingRequestParameter('q');
|
||||
}
|
||||
if (!publicKey.g) {
|
||||
throw error.missingRequestParameter('g');
|
||||
}
|
||||
if (sessionToken.deviceId) {
|
||||
deviceId = sessionToken.deviceId;
|
||||
} else if (!service || service === 'sync') {
|
||||
// Synthesize a device record for Sync sessions that don't already have one.
|
||||
// Include the UA info so that we can synthesize a device name
|
||||
// for any push notifications.
|
||||
const deviceInfo = {
|
||||
uaBrowser: sessionToken.uaBrowser,
|
||||
uaBrowserVersion: sessionToken.uaBrowserVersion,
|
||||
uaOS: sessionToken.uaOS,
|
||||
uaOSVersion: sessionToken.uaOSVersion,
|
||||
};
|
||||
try {
|
||||
const result = await devices.upsert(
|
||||
request,
|
||||
sessionToken,
|
||||
deviceInfo
|
||||
);
|
||||
deviceId = result.id;
|
||||
} catch (err) {
|
||||
// There's a small chance that a device registration was performed
|
||||
// concurrently. If so, just use that device id.
|
||||
if (err.errno !== error.ERRNO.DEVICE_CONFLICT) {
|
||||
throw err;
|
||||
}
|
||||
deviceId = err.output.payload.deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sessionToken.locale) {
|
||||
if (request.app.acceptLanguage) {
|
||||
// Log details to sanity-check locale backfilling.
|
||||
log.info('signer.updateLocale', {
|
||||
locale: request.app.acceptLanguage,
|
||||
});
|
||||
db.updateLocale(sessionToken.uid, request.app.acceptLanguage);
|
||||
// meh on the result
|
||||
} else {
|
||||
// We're seeing a surprising number of accounts that don't get
|
||||
// a proper locale. Log details to help debug this.
|
||||
log.info('signer.emptyLocale', {
|
||||
email: sessionToken.email,
|
||||
locale: request.app.acceptLanguage,
|
||||
agent: request.headers['user-agent'],
|
||||
});
|
||||
}
|
||||
}
|
||||
uid = sessionToken.uid;
|
||||
if (publicKey.algorithm === 'RS') {
|
||||
if (!publicKey.n) {
|
||||
throw error.missingRequestParameter('n');
|
||||
}
|
||||
if (!publicKey.e) {
|
||||
throw error.missingRequestParameter('e');
|
||||
}
|
||||
} else {
|
||||
// DS
|
||||
if (!publicKey.y) {
|
||||
throw error.missingRequestParameter('y');
|
||||
}
|
||||
if (!publicKey.p) {
|
||||
throw error.missingRequestParameter('p');
|
||||
}
|
||||
if (!publicKey.q) {
|
||||
throw error.missingRequestParameter('q');
|
||||
}
|
||||
if (!publicKey.g) {
|
||||
throw error.missingRequestParameter('g');
|
||||
}
|
||||
}
|
||||
|
||||
return signer.sign({
|
||||
email: `${uid}@${domain}`,
|
||||
publicKey: publicKey,
|
||||
domain: domain,
|
||||
duration: duration,
|
||||
generation: sessionToken.verifierSetAt,
|
||||
lastAuthAt: sessionToken.lastAuthAt(),
|
||||
verifiedEmail: sessionToken.email,
|
||||
deviceId: deviceId,
|
||||
tokenVerified: sessionToken.tokenVerified,
|
||||
authenticationMethods: Array.from(
|
||||
sessionToken.authenticationMethods
|
||||
),
|
||||
authenticatorAssuranceLevel:
|
||||
sessionToken.authenticatorAssuranceLevel,
|
||||
profileChangedAt: sessionToken.profileChangedAt,
|
||||
if (!sessionToken.locale) {
|
||||
if (request.app.acceptLanguage) {
|
||||
// Log details to sanity-check locale backfilling.
|
||||
log.info('signer.updateLocale', {
|
||||
locale: request.app.acceptLanguage,
|
||||
});
|
||||
})
|
||||
.then(result => {
|
||||
certResult = result;
|
||||
return request.emitMetricsEvent('account.signed', {
|
||||
uid: uid,
|
||||
device_id: deviceId,
|
||||
db.updateLocale(sessionToken.uid, request.app.acceptLanguage);
|
||||
// meh on the result
|
||||
} else {
|
||||
// We're seeing a surprising number of accounts that don't get
|
||||
// a proper locale. Log details to help debug this.
|
||||
log.info('signer.emptyLocale', {
|
||||
email: sessionToken.email,
|
||||
locale: request.app.acceptLanguage,
|
||||
agent: request.headers['user-agent'],
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return certResult;
|
||||
});
|
||||
}
|
||||
}
|
||||
const uid = sessionToken.uid;
|
||||
|
||||
const certResult = await signer.sign({
|
||||
email: `${uid}@${domain}`,
|
||||
publicKey: publicKey,
|
||||
domain: domain,
|
||||
duration: duration,
|
||||
generation: sessionToken.verifierSetAt,
|
||||
lastAuthAt: sessionToken.lastAuthAt(),
|
||||
verifiedEmail: sessionToken.email,
|
||||
deviceId: deviceId,
|
||||
tokenVerified: sessionToken.tokenVerified,
|
||||
authenticationMethods: Array.from(sessionToken.authenticationMethods),
|
||||
authenticatorAssuranceLevel: sessionToken.authenticatorAssuranceLevel,
|
||||
profileChangedAt: sessionToken.profileChangedAt,
|
||||
});
|
||||
request.emitMetricsEvent('account.signed', {
|
||||
uid: uid,
|
||||
device_id: deviceId,
|
||||
});
|
||||
return certResult;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -133,10 +133,7 @@ module.exports = (log, db, config, customs, push, oauthdb, subhub) => {
|
|||
if (!selectedPlan) {
|
||||
throw error.unknownSubscriptionPlan(planId);
|
||||
}
|
||||
// TODO: The FxA DB has a column `productName` that we're using for
|
||||
// product_id. We might want to rename that someday.
|
||||
// https://github.com/mozilla/fxa/issues/1187
|
||||
const productName = selectedPlan.product_id;
|
||||
const productId = selectedPlan.product_id;
|
||||
|
||||
const paymentResult = await subhub.createSubscription(
|
||||
uid,
|
||||
|
@ -156,7 +153,7 @@ module.exports = (log, db, config, customs, push, oauthdb, subhub) => {
|
|||
await db.createAccountSubscription({
|
||||
uid,
|
||||
subscriptionId,
|
||||
productName,
|
||||
productId,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
const errors = require('../error');
|
||||
const validators = require('./validators');
|
||||
const isA = require('joi');
|
||||
const P = require('../promise');
|
||||
const otplib = require('otplib');
|
||||
const qrcode = require('qrcode');
|
||||
const { promisify } = require('util');
|
||||
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema;
|
||||
|
||||
module.exports = (log, db, mailer, customs, config) => {
|
||||
|
@ -31,7 +31,7 @@ module.exports = (log, db, mailer, customs, config) => {
|
|||
const RECOVERY_CODE_COUNT =
|
||||
(config.recoveryCodes && config.recoveryCodes.count) || 8;
|
||||
|
||||
P.promisify(qrcode.toDataURL);
|
||||
promisify(qrcode.toDataURL);
|
||||
|
||||
return [
|
||||
{
|
||||
|
@ -56,52 +56,36 @@ module.exports = (log, db, mailer, customs, config) => {
|
|||
handler: async function(request) {
|
||||
log.begin('totp.create', request);
|
||||
|
||||
let response;
|
||||
let secret;
|
||||
const sessionToken = request.auth.credentials;
|
||||
const uid = sessionToken.uid;
|
||||
|
||||
const authenticator = new otplib.authenticator.Authenticator();
|
||||
authenticator.options = otplib.authenticator.options;
|
||||
|
||||
return customs
|
||||
.check(request, sessionToken.email, 'totpCreate')
|
||||
.then(() => {
|
||||
secret = authenticator.generateSecret();
|
||||
return createTotpToken();
|
||||
})
|
||||
.then(emitMetrics)
|
||||
.then(createResponse)
|
||||
.then(() => response);
|
||||
await customs.check(request, sessionToken.email, 'totpCreate');
|
||||
|
||||
function createTotpToken() {
|
||||
if (sessionToken.tokenVerificationId) {
|
||||
throw errors.unverifiedSession();
|
||||
}
|
||||
|
||||
return db.createTotpToken(uid, secret, 0);
|
||||
if (sessionToken.tokenVerificationId) {
|
||||
throw errors.unverifiedSession();
|
||||
}
|
||||
|
||||
function createResponse() {
|
||||
const otpauth = authenticator.keyuri(
|
||||
sessionToken.email,
|
||||
config.serviceName,
|
||||
secret
|
||||
);
|
||||
const secret = authenticator.generateSecret();
|
||||
await db.createTotpToken(uid, secret, 0);
|
||||
|
||||
return qrcode.toDataURL(otpauth, qrCodeOptions).then(qrCodeUrl => {
|
||||
response = {
|
||||
qrCodeUrl,
|
||||
secret,
|
||||
};
|
||||
});
|
||||
}
|
||||
log.info('totpToken.created', { uid });
|
||||
await request.emitMetricsEvent('totpToken.created', { uid });
|
||||
|
||||
function emitMetrics() {
|
||||
log.info('totpToken.created', {
|
||||
uid: uid,
|
||||
});
|
||||
return request.emitMetricsEvent('totpToken.created', { uid: uid });
|
||||
}
|
||||
const otpauth = authenticator.keyuri(
|
||||
sessionToken.email,
|
||||
config.serviceName,
|
||||
secret
|
||||
);
|
||||
|
||||
const qrCodeUrl = await qrcode.toDataURL(otpauth, qrCodeOptions);
|
||||
|
||||
return {
|
||||
qrCodeUrl,
|
||||
secret,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -117,69 +101,57 @@ module.exports = (log, db, mailer, customs, config) => {
|
|||
log.begin('totp.destroy', request);
|
||||
|
||||
const sessionToken = request.auth.credentials;
|
||||
const uid = sessionToken.uid;
|
||||
let hasEnabledToken = false;
|
||||
const { uid } = sessionToken;
|
||||
|
||||
return customs
|
||||
.check(request, sessionToken.email, 'totpDestroy')
|
||||
.then(checkTotpToken)
|
||||
.then(deleteTotpToken)
|
||||
.then(sendEmailNotification)
|
||||
.then(() => {
|
||||
return {};
|
||||
});
|
||||
await customs.check(request, sessionToken.email, 'totpDestroy');
|
||||
|
||||
function checkTotpToken() {
|
||||
// If a TOTP token is not verified, we should be able to safely delete regardless of session
|
||||
// verification state.
|
||||
return totpUtils
|
||||
.hasTotpToken({ uid })
|
||||
.then(result => (hasEnabledToken = result));
|
||||
// If a TOTP token is not verified, we should be able to safely delete regardless of session
|
||||
// verification state.
|
||||
const hasEnabledToken = await totpUtils.hasTotpToken({ uid });
|
||||
|
||||
// To help prevent users from getting locked out of their account, sessions created and verified
|
||||
// before TOTP was enabled, can remove TOTP. Any new sessions after TOTP is enabled, are only considered
|
||||
// verified *if and only if* they have verified a TOTP code.
|
||||
if (!sessionToken.tokenVerified) {
|
||||
throw errors.unverifiedSession();
|
||||
}
|
||||
|
||||
function deleteTotpToken() {
|
||||
// To help prevent users from getting locked out of their account, sessions created and verified
|
||||
// before TOTP was enabled, can remove TOTP. Any new sessions after TOTP is enabled, are only considered
|
||||
// verified *if and only if* they have verified a TOTP code.
|
||||
if (!sessionToken.tokenVerified) {
|
||||
throw errors.unverifiedSession();
|
||||
}
|
||||
await db.deleteTotpToken(uid);
|
||||
|
||||
return db.deleteTotpToken(uid).then(() => {
|
||||
return log.notifyAttachedServices('profileDataChanged', request, {
|
||||
uid: sessionToken.uid,
|
||||
});
|
||||
});
|
||||
}
|
||||
await log.notifyAttachedServices('profileDataChanged', request, {
|
||||
uid,
|
||||
});
|
||||
|
||||
function sendEmailNotification() {
|
||||
if (!hasEnabledToken) {
|
||||
return;
|
||||
}
|
||||
if (hasEnabledToken) {
|
||||
const account = await db.account(uid);
|
||||
const geoData = request.app.geo;
|
||||
const ip = request.app.clientAddress;
|
||||
const emailOptions = {
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
ip,
|
||||
location: geoData.location,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser: request.app.ua.browser,
|
||||
uaBrowserVersion: request.app.ua.browserVersion,
|
||||
uaOS: request.app.ua.os,
|
||||
uaOSVersion: request.app.ua.osVersion,
|
||||
uaDeviceType: request.app.ua.deviceType,
|
||||
uid,
|
||||
};
|
||||
|
||||
return db.account(sessionToken.uid).then(account => {
|
||||
const geoData = request.app.geo;
|
||||
const ip = request.app.clientAddress;
|
||||
const emailOptions = {
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
ip: ip,
|
||||
location: geoData.location,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser: request.app.ua.browser,
|
||||
uaBrowserVersion: request.app.ua.browserVersion,
|
||||
uaOS: request.app.ua.os,
|
||||
uaOSVersion: request.app.ua.osVersion,
|
||||
uaDeviceType: request.app.ua.deviceType,
|
||||
uid: sessionToken.uid,
|
||||
};
|
||||
|
||||
mailer.sendPostRemoveTwoStepAuthNotification(
|
||||
try {
|
||||
await mailer.sendPostRemoveTwoStepAuthNotification(
|
||||
account.emails,
|
||||
account,
|
||||
emailOptions
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
// If email fails, log the error without aborting the operation.
|
||||
log.error('mailer.sendPostRemoveTwoStepAuthNotification', { err });
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -201,42 +173,30 @@ module.exports = (log, db, mailer, customs, config) => {
|
|||
const sessionToken = request.auth.credentials;
|
||||
let exists = false;
|
||||
|
||||
return getTotpToken().then(() => {
|
||||
return { exists };
|
||||
});
|
||||
|
||||
function getTotpToken() {
|
||||
return P.resolve()
|
||||
.then(() => {
|
||||
if (sessionToken.tokenVerificationId) {
|
||||
throw errors.unverifiedSession();
|
||||
}
|
||||
|
||||
return db.totpToken(sessionToken.uid);
|
||||
})
|
||||
|
||||
.then(
|
||||
token => {
|
||||
// If the token is not verified, lets delete it and report that
|
||||
// it doesn't exist. This will help prevent some edge
|
||||
// cases where the user started creating a token but never completed.
|
||||
if (!token.verified) {
|
||||
return db.deleteTotpToken(sessionToken.uid).then(() => {
|
||||
exists = false;
|
||||
});
|
||||
} else {
|
||||
exists = true;
|
||||
}
|
||||
},
|
||||
err => {
|
||||
if (err.errno === errors.ERRNO.TOTP_TOKEN_NOT_FOUND) {
|
||||
exists = false;
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
if (sessionToken.tokenVerificationId) {
|
||||
throw errors.unverifiedSession();
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await db.totpToken(sessionToken.uid);
|
||||
|
||||
// If the token is not verified, lets delete it and report that
|
||||
// it doesn't exist. This will help prevent some edge
|
||||
// cases where the user started creating a token but never completed.
|
||||
if (!token.verified) {
|
||||
await db.deleteTotpToken(sessionToken.uid);
|
||||
} else {
|
||||
exists = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.errno === errors.ERRNO.TOTP_TOKEN_NOT_FOUND) {
|
||||
exists = false;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return { exists };
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -271,141 +231,110 @@ module.exports = (log, db, mailer, customs, config) => {
|
|||
|
||||
const code = request.payload.code;
|
||||
const sessionToken = request.auth.credentials;
|
||||
const uid = sessionToken.uid;
|
||||
const email = sessionToken.email;
|
||||
let sharedSecret, isValidCode, tokenVerified, recoveryCodes;
|
||||
const { uid, email } = sessionToken;
|
||||
let recoveryCodes;
|
||||
|
||||
return customs
|
||||
.check(request, email, 'verifyTotpCode')
|
||||
.then(getTotpToken)
|
||||
.then(verifyTotpCode)
|
||||
.then(verifyTotpToken)
|
||||
.then(replaceRecoveryCodes)
|
||||
.then(verifySession)
|
||||
.then(emitMetrics)
|
||||
.then(sendEmailNotification)
|
||||
.then(() => {
|
||||
const response = {
|
||||
success: isValidCode,
|
||||
};
|
||||
await customs.check(request, email, 'verifyTotpCode');
|
||||
|
||||
if (recoveryCodes) {
|
||||
response.recoveryCodes = recoveryCodes;
|
||||
}
|
||||
const token = await db.totpToken(sessionToken.uid);
|
||||
const sharedSecret = token.sharedSecret;
|
||||
const tokenVerified = token.verified;
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
function getTotpToken() {
|
||||
return db.totpToken(sessionToken.uid).then(token => {
|
||||
sharedSecret = token.sharedSecret;
|
||||
tokenVerified = token.verified;
|
||||
});
|
||||
}
|
||||
|
||||
function verifyTotpCode() {
|
||||
const authenticator = new otplib.authenticator.Authenticator();
|
||||
authenticator.options = Object.assign(
|
||||
{},
|
||||
otplib.authenticator.options,
|
||||
{ secret: sharedSecret }
|
||||
);
|
||||
isValidCode = authenticator.check(code, sharedSecret);
|
||||
}
|
||||
const authenticator = new otplib.authenticator.Authenticator();
|
||||
authenticator.options = Object.assign(
|
||||
{},
|
||||
otplib.authenticator.options,
|
||||
{ secret: sharedSecret }
|
||||
);
|
||||
const isValidCode = authenticator.check(code, sharedSecret);
|
||||
|
||||
// Once a valid TOTP code has been detected, the token becomes verified
|
||||
// and enabled for the user.
|
||||
function verifyTotpToken() {
|
||||
if (isValidCode && !tokenVerified) {
|
||||
return db
|
||||
.updateTotpToken(sessionToken.uid, {
|
||||
verified: true,
|
||||
enabled: true,
|
||||
})
|
||||
.then(() => {
|
||||
return log.notifyAttachedServices(
|
||||
'profileDataChanged',
|
||||
request,
|
||||
{
|
||||
uid: sessionToken.uid,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
if (isValidCode && !tokenVerified) {
|
||||
await db.updateTotpToken(sessionToken.uid, {
|
||||
verified: true,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
await log.notifyAttachedServices('profileDataChanged', request, {
|
||||
uid: sessionToken.uid,
|
||||
});
|
||||
}
|
||||
|
||||
// If this is a new registration, replace and generate recovery codes
|
||||
function replaceRecoveryCodes() {
|
||||
if (isValidCode && !tokenVerified) {
|
||||
return db
|
||||
.replaceRecoveryCodes(uid, RECOVERY_CODE_COUNT)
|
||||
.then(result => (recoveryCodes = result));
|
||||
}
|
||||
if (isValidCode && !tokenVerified) {
|
||||
recoveryCodes = await db.replaceRecoveryCodes(
|
||||
uid,
|
||||
RECOVERY_CODE_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
// If a valid code was sent, this verifies the session using the `totp-2fa` method.
|
||||
function verifySession() {
|
||||
if (isValidCode && sessionToken.authenticatorAssuranceLevel <= 1) {
|
||||
return db.verifyTokensWithMethod(sessionToken.id, 'totp-2fa');
|
||||
}
|
||||
if (isValidCode && sessionToken.authenticatorAssuranceLevel <= 1) {
|
||||
await db.verifyTokensWithMethod(sessionToken.id, 'totp-2fa');
|
||||
}
|
||||
|
||||
function emitMetrics() {
|
||||
if (isValidCode) {
|
||||
log.info('totp.verified', {
|
||||
uid: uid,
|
||||
});
|
||||
request.emitMetricsEvent('totpToken.verified', { uid: uid });
|
||||
} else {
|
||||
log.info('totp.unverified', {
|
||||
uid: uid,
|
||||
});
|
||||
request.emitMetricsEvent('totpToken.unverified', { uid: uid });
|
||||
}
|
||||
if (isValidCode) {
|
||||
log.info('totp.verified', { uid });
|
||||
await request.emitMetricsEvent('totpToken.verified', { uid });
|
||||
} else {
|
||||
log.info('totp.unverified', { uid });
|
||||
await request.emitMetricsEvent('totpToken.unverified', { uid });
|
||||
}
|
||||
|
||||
function sendEmailNotification() {
|
||||
return db.account(sessionToken.uid).then(account => {
|
||||
const geoData = request.app.geo;
|
||||
const ip = request.app.clientAddress;
|
||||
const service = request.payload.service || request.query.service;
|
||||
const emailOptions = {
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
ip: ip,
|
||||
location: geoData.location,
|
||||
service: service,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser: request.app.ua.browser,
|
||||
uaBrowserVersion: request.app.ua.browserVersion,
|
||||
uaOS: request.app.ua.os,
|
||||
uaOSVersion: request.app.ua.osVersion,
|
||||
uaDeviceType: request.app.ua.deviceType,
|
||||
uid: sessionToken.uid,
|
||||
};
|
||||
await sendEmailNotification();
|
||||
|
||||
// Check to see if this token was just verified, if it is, then this means
|
||||
// the user has enabled two step authentication, otherwise send new device
|
||||
// login email.
|
||||
if (isValidCode && !tokenVerified) {
|
||||
return mailer.sendPostAddTwoStepAuthNotification(
|
||||
account.emails,
|
||||
account,
|
||||
emailOptions
|
||||
);
|
||||
}
|
||||
const response = {
|
||||
success: isValidCode,
|
||||
};
|
||||
|
||||
// All accounts that have a TOTP token, force the session to be verified, therefore
|
||||
// we can not check `session.mustVerify=true` to determine sending the new device
|
||||
// login email. Instead, lets perform a basic check that the service is `sync`, otherwise
|
||||
// don't send.
|
||||
if (isValidCode && service === 'sync') {
|
||||
return mailer.sendNewDeviceLoginNotification(
|
||||
account.emails,
|
||||
account,
|
||||
emailOptions
|
||||
);
|
||||
}
|
||||
});
|
||||
if (recoveryCodes) {
|
||||
response.recoveryCodes = recoveryCodes;
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
async function sendEmailNotification() {
|
||||
const account = await db.account(sessionToken.uid);
|
||||
const geoData = request.app.geo;
|
||||
const ip = request.app.clientAddress;
|
||||
const service = request.payload.service || request.query.service;
|
||||
const emailOptions = {
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
ip: ip,
|
||||
location: geoData.location,
|
||||
service: service,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser: request.app.ua.browser,
|
||||
uaBrowserVersion: request.app.ua.browserVersion,
|
||||
uaOS: request.app.ua.os,
|
||||
uaOSVersion: request.app.ua.osVersion,
|
||||
uaDeviceType: request.app.ua.deviceType,
|
||||
uid: sessionToken.uid,
|
||||
};
|
||||
|
||||
// Check to see if this token was just verified, if it is, then this means
|
||||
// the user has enabled two step authentication, otherwise send new device
|
||||
// login email.
|
||||
if (isValidCode && !tokenVerified) {
|
||||
return mailer.sendPostAddTwoStepAuthNotification(
|
||||
account.emails,
|
||||
account,
|
||||
emailOptions
|
||||
);
|
||||
}
|
||||
|
||||
// All accounts that have a TOTP token, force the session to be verified, therefore
|
||||
// we can not check `session.mustVerify=true` to determine sending the new device
|
||||
// login email. Instead, lets perform a basic check that the service is `sync`, otherwise
|
||||
// don't send.
|
||||
if (isValidCode && service === 'sync') {
|
||||
return mailer.sendNewDeviceLoginNotification(
|
||||
account.emails,
|
||||
account,
|
||||
emailOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -35,53 +35,48 @@ module.exports = (log, db, mailer, config, customs) => {
|
|||
|
||||
const { flowId, flowBeginTime } = await request.app.metricsContext;
|
||||
|
||||
return customs
|
||||
.check(request, email, 'sendUnblockCode')
|
||||
.then(lookupAccount)
|
||||
.then(createUnblockCode)
|
||||
.then(mailUnblockCode)
|
||||
.then(() => request.emitMetricsEvent('account.login.sentUnblockCode'))
|
||||
.then(() => {
|
||||
return {};
|
||||
});
|
||||
await customs.check(request, email, 'sendUnblockCode');
|
||||
const uid = await lookupAccount();
|
||||
const code = await createUnblockCode(uid);
|
||||
await mailUnblockCode(code);
|
||||
await request.emitMetricsEvent('account.login.sentUnblockCode');
|
||||
return {};
|
||||
|
||||
function lookupAccount() {
|
||||
return db.accountRecord(email).then(record => {
|
||||
emailRecord = record;
|
||||
return record.uid;
|
||||
});
|
||||
async function lookupAccount() {
|
||||
const record = await db.accountRecord(email);
|
||||
emailRecord = record;
|
||||
return record.uid;
|
||||
}
|
||||
|
||||
function createUnblockCode(uid) {
|
||||
async function createUnblockCode(uid) {
|
||||
return db.createUnblockCode(uid);
|
||||
}
|
||||
|
||||
function mailUnblockCode(code) {
|
||||
return db.accountEmails(emailRecord.uid).then(emails => {
|
||||
const geoData = request.app.geo;
|
||||
const {
|
||||
browser: uaBrowser,
|
||||
browserVersion: uaBrowserVersion,
|
||||
os: uaOS,
|
||||
osVersion: uaOSVersion,
|
||||
deviceType: uaDeviceType,
|
||||
} = request.app.ua;
|
||||
async function mailUnblockCode(code) {
|
||||
const emails = await db.accountEmails(emailRecord.uid);
|
||||
const geoData = request.app.geo;
|
||||
const {
|
||||
browser: uaBrowser,
|
||||
browserVersion: uaBrowserVersion,
|
||||
os: uaOS,
|
||||
osVersion: uaOSVersion,
|
||||
deviceType: uaDeviceType,
|
||||
} = request.app.ua;
|
||||
|
||||
return mailer.sendUnblockCode(emails, emailRecord, {
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
unblockCode: code,
|
||||
flowId,
|
||||
flowBeginTime,
|
||||
ip: request.app.clientAddress,
|
||||
location: geoData.location,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser,
|
||||
uaBrowserVersion,
|
||||
uaOS,
|
||||
uaOSVersion,
|
||||
uaDeviceType,
|
||||
uid: emailRecord.uid,
|
||||
});
|
||||
return mailer.sendUnblockCode(emails, emailRecord, {
|
||||
acceptLanguage: request.app.acceptLanguage,
|
||||
unblockCode: code,
|
||||
flowId,
|
||||
flowBeginTime,
|
||||
ip: request.app.clientAddress,
|
||||
location: geoData.location,
|
||||
timeZone: geoData.timeZone,
|
||||
uaBrowser,
|
||||
uaBrowserVersion,
|
||||
uaOS,
|
||||
uaOSVersion,
|
||||
uaDeviceType,
|
||||
uid: emailRecord.uid,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -111,13 +106,12 @@ module.exports = (log, db, mailer, config, customs) => {
|
|||
const uid = request.payload.uid;
|
||||
const code = request.payload.unblockCode.toUpperCase();
|
||||
|
||||
return db.consumeUnblockCode(uid, code).then(() => {
|
||||
log.info('account.login.rejectedUnblockCode', {
|
||||
uid,
|
||||
unblockCode: code,
|
||||
});
|
||||
return {};
|
||||
await db.consumeUnblockCode(uid, code);
|
||||
log.info('account.login.rejectedUnblockCode', {
|
||||
uid,
|
||||
unblockCode: code,
|
||||
});
|
||||
return {};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -15,14 +15,13 @@ module.exports = (log, config, redirectDomain) => {
|
|||
method: 'POST',
|
||||
path: '/get_random_bytes',
|
||||
handler: async function getRandomBytes(request) {
|
||||
return random(32).then(
|
||||
bytes => {
|
||||
return { data: bytes.toString('hex') };
|
||||
},
|
||||
err => {
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
let bytes;
|
||||
try {
|
||||
bytes = await random(32);
|
||||
return { data: bytes.toString('hex') };
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -28,10 +28,7 @@ module.exports = {
|
|||
// All accounts get this product
|
||||
PRODUCT_REGISTERED,
|
||||
// Other products come from actual subscriptions
|
||||
// TODO: The FxA DB has a column `productName` that we're using for
|
||||
// product_id. We might want to rename that someday.
|
||||
// https://github.com/mozilla/fxa/issues/1187
|
||||
...subscriptions.map(({ productName }) => productName),
|
||||
...subscriptions.map(({ productId }) => productId),
|
||||
];
|
||||
// Accounts with at least one subscription get this product
|
||||
if (subscriptions.length > 0) {
|
||||
|
|
|
@ -292,10 +292,7 @@ module.exports.subscriptionsPaymentToken = isA.string().max(255);
|
|||
module.exports.activeSubscriptionValidator = isA.object({
|
||||
uid: isA.string().required(),
|
||||
subscriptionId: module.exports.subscriptionsSubscriptionId.required(),
|
||||
// TODO: The FxA DB has a column `productName` that we're using for
|
||||
// product ID. We might want to rename that someday.
|
||||
// https://github.com/mozilla/fxa/issues/1187
|
||||
productName: module.exports.subscriptionsProductId.required(),
|
||||
productId: module.exports.subscriptionsProductId.required(),
|
||||
createdAt: isA.number().required(),
|
||||
cancelledAt: isA.alternatives(isA.number(), isA.any().allow(null)),
|
||||
});
|
||||
|
|
|
@ -90,7 +90,7 @@ module.exports = function(log, config) {
|
|||
eventCreatedAt: message.eventCreatedAt,
|
||||
subscriptionId: message.subscriptionId,
|
||||
isActive: message.active,
|
||||
productName: message.productName,
|
||||
productId: message.productName,
|
||||
productCapabilities:
|
||||
config.subscriptions.productCapabilities[message.productName] ||
|
||||
[],
|
||||
|
|
|
@ -171,32 +171,6 @@
|
|||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@newrelic/koa": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@newrelic/koa/-/koa-1.0.8.tgz",
|
||||
"integrity": "sha512-kY//FlLQkGdUIKEeGJlyY3dJRU63EG77YIa48ACMGZxQbWRd3WZMikyft33f8XScTq6WpCDo9xa0viNo8zeYkg==",
|
||||
"requires": {
|
||||
"methods": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"@newrelic/native-metrics": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@newrelic/native-metrics/-/native-metrics-3.1.2.tgz",
|
||||
"integrity": "sha512-JjUmPrp2LEEkhVtelICme5p7sHHpfpu2Wjk5/L1D3Zvt01v4mCsrL2XaIMBmHgg3T2ZbqMiqWZCn2LtGZ6nklA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"nan": "^2.10.0",
|
||||
"semver": "^5.5.1"
|
||||
}
|
||||
},
|
||||
"@newrelic/superagent": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@newrelic/superagent/-/superagent-1.0.3.tgz",
|
||||
"integrity": "sha512-lJbsqKa79qPLbHZsbiRaXl1jfzaXAN7zqqnLRqBY+zI/O5zcfyNngTmdi+9y+qIUq7xHYNaLsAxCXerrsoINKg==",
|
||||
"requires": {
|
||||
"methods": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz",
|
||||
|
@ -241,11 +215,6 @@
|
|||
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@tyriar/fibonacci-heap": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tyriar/fibonacci-heap/-/fibonacci-heap-2.0.9.tgz",
|
||||
"integrity": "sha512-bYuSNomfn4hu2tPiDN+JZtnzCpSpbJ/PNeulmocDy3xN2X5OkJL65zo6rPZp65cPPhLF9vfT/dgE+RtFRCSxOA=="
|
||||
},
|
||||
"a-sync-waterfall": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
|
||||
|
@ -275,14 +244,6 @@
|
|||
"resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz",
|
||||
"integrity": "sha1-R6++GiqSYhkdtoOOT9HTm0CCF0Y="
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
|
||||
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
|
||||
"requires": {
|
||||
"es6-promisify": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-4.1.7.tgz",
|
||||
|
@ -805,7 +766,8 @@
|
|||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
|
||||
"dev": true
|
||||
},
|
||||
"buildmail": {
|
||||
"version": "4.0.1",
|
||||
|
@ -1109,6 +1071,7 @@
|
|||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
|
@ -1450,19 +1413,6 @@
|
|||
"event-emitter": "~0.3.5"
|
||||
}
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
|
||||
"requires": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"es6-set": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz",
|
||||
|
@ -3417,30 +3367,6 @@
|
|||
"resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.4.24.tgz",
|
||||
"integrity": "sha1-QzX/2CzZaWaKOUZckprGHWOTYn8="
|
||||
},
|
||||
"https-proxy-agent": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz",
|
||||
"integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==",
|
||||
"requires": {
|
||||
"agent-base": "^4.3.0",
|
||||
"debug": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"i18n-abide": {
|
||||
"version": "0.0.26",
|
||||
"resolved": "https://registry.npmjs.org/i18n-abide/-/i18n-abide-0.0.26.tgz",
|
||||
|
@ -4436,11 +4362,6 @@
|
|||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
|
||||
"dev": true
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"micromatch": {
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
|
||||
|
@ -4823,23 +4744,6 @@
|
|||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
|
||||
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
|
||||
},
|
||||
"newrelic": {
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/newrelic/-/newrelic-4.10.0.tgz",
|
||||
"integrity": "sha512-ih/tdO5jMcZ7oYiL7xtES4RBPp7EzBg5Shj8VWBmJ2lOxjWNFwTJ7EqQ7Q0U1xK1zkuZ6Gp5q37Un/N3+QAMFw==",
|
||||
"requires": {
|
||||
"@newrelic/koa": "^1.0.0",
|
||||
"@newrelic/native-metrics": "^3.0.0",
|
||||
"@newrelic/superagent": "^1.0.0",
|
||||
"@tyriar/fibonacci-heap": "^2.0.7",
|
||||
"async": "^2.1.4",
|
||||
"concat-stream": "^1.5.0",
|
||||
"https-proxy-agent": "^2.2.1",
|
||||
"json-stringify-safe": "^5.0.0",
|
||||
"readable-stream": "^2.1.4",
|
||||
"semver": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"next-tick": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
|
||||
|
@ -7753,10 +7657,11 @@
|
|||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||
"dev": true
|
||||
},
|
||||
"uap-core": {
|
||||
"version": "git://github.com/ua-parser/uap-core.git#286809e09706ea891b9434ed875574d65e0ff6b7",
|
||||
"version": "git://github.com/ua-parser/uap-core.git#2e6c983e42e7aae7d957a263cb4d3de7ccbd92af",
|
||||
"from": "git://github.com/ua-parser/uap-core.git"
|
||||
},
|
||||
"uap-ref-impl": {
|
||||
|
|
|
@ -71,7 +71,6 @@
|
|||
"mozlog": "2.2.0",
|
||||
"mysql": "2.15.0",
|
||||
"mysql-patcher": "0.7.0",
|
||||
"newrelic": "4.10.0",
|
||||
"node-uap": "git+https://github.com/vladikoff/node-uap.git#9cdd16247",
|
||||
"node-zendesk": "^1.4.0",
|
||||
"nodemailer": "2.7.2",
|
||||
|
|
|
@ -32,7 +32,7 @@ describe('lib/redis:', () => {
|
|||
};
|
||||
fxaShared = sinon.spy(() => redis);
|
||||
wrapper = proxyquire(`${LIB_DIR}/redis`, {
|
||||
'../../../fxa-shared/redis': fxaShared,
|
||||
'../../fxa-shared/redis': fxaShared,
|
||||
})(config, log);
|
||||
});
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ const ACTIVE_SUBSCRIPTIONS = [
|
|||
{
|
||||
uid: UID,
|
||||
subscriptionId: SUBSCRIPTION_ID_1,
|
||||
productName: PLANS[0].product_id,
|
||||
productId: PLANS[0].product_id,
|
||||
createdAt: NOW,
|
||||
cancelledAt: null,
|
||||
},
|
||||
|
@ -275,10 +275,7 @@ describe('subscriptions', () => {
|
|||
assert.deepEqual(createArgs, {
|
||||
uid: UID,
|
||||
subscriptionId: SUBSCRIPTION_ID_1,
|
||||
// TODO: The FxA DB has a column `productName` that we're using for
|
||||
// product_id. We might want to rename that someday.
|
||||
// https://github.com/mozilla/fxa/issues/1187
|
||||
productName: PLANS[0].product_id,
|
||||
productId: PLANS[0].product_id,
|
||||
createdAt: createArgs.createdAt,
|
||||
});
|
||||
assert.deepEqual(res, { subscriptionId: SUBSCRIPTION_ID_1 });
|
||||
|
|
|
@ -20,13 +20,13 @@ const MOCK_SUBSCRIPTIONS = [
|
|||
{
|
||||
uid: UID,
|
||||
subscriptionId: 'sub1',
|
||||
productName: 'p1',
|
||||
productId: 'p1',
|
||||
createdAt: NOW,
|
||||
},
|
||||
{
|
||||
uid: UID,
|
||||
subscriptionId: 'sub2',
|
||||
productName: 'p2',
|
||||
productId: 'p2',
|
||||
createdAt: NOW,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -100,7 +100,7 @@ describe('subhub updates', () => {
|
|||
uid: baseMessage.uid,
|
||||
subscriptionId: baseMessage.subscriptionId,
|
||||
isActive: true,
|
||||
productName: baseMessage.productName,
|
||||
productId: baseMessage.productName,
|
||||
productCapabilities: ['foo', 'bar'],
|
||||
});
|
||||
|
||||
|
@ -139,7 +139,7 @@ describe('subhub updates', () => {
|
|||
uid: baseMessage.uid,
|
||||
subscriptionId: baseMessage.subscriptionId,
|
||||
isActive: false,
|
||||
productName: baseMessage.productName,
|
||||
productId: baseMessage.productName,
|
||||
productCapabilities: ['foo', 'bar'],
|
||||
});
|
||||
});
|
||||
|
@ -153,7 +153,7 @@ describe('subhub updates', () => {
|
|||
return {
|
||||
uid,
|
||||
subscriptionId,
|
||||
productName: message.productName,
|
||||
productId: message.productName,
|
||||
// It's a subscription FROM THE FUTURE!
|
||||
createdAt: message.eventCreatedAt + 1000,
|
||||
};
|
||||
|
@ -184,7 +184,7 @@ describe('subhub updates', () => {
|
|||
return {
|
||||
uid,
|
||||
subscriptionId,
|
||||
productName: message.productName,
|
||||
productId: message.productName,
|
||||
// It's a subscription FROM THE FUTURE!
|
||||
createdAt: message.eventCreatedAt + 1000,
|
||||
};
|
||||
|
|
|
@ -209,7 +209,7 @@ describe('remote subscriptions:', function() {
|
|||
assert.lengthOf(result, 1);
|
||||
assert.isAbove(result[0].createdAt, Date.now() - 1000);
|
||||
assert.isAtMost(result[0].createdAt, Date.now());
|
||||
assert.equal(result[0].productName, PRODUCT_ID);
|
||||
assert.equal(result[0].productId, PRODUCT_ID);
|
||||
assert.equal(result[0].uid, client.uid);
|
||||
assert.isNull(result[0].cancelledAt);
|
||||
|
||||
|
@ -260,7 +260,7 @@ describe('remote subscriptions:', function() {
|
|||
assert.isAbove(result[0].createdAt, Date.now() - 1000);
|
||||
assert.isAtLeast(result[0].cancelledAt, result[0].createdAt);
|
||||
assert.isAtMost(result[0].cancelledAt, Date.now());
|
||||
assert.equal(result[0].productName, PRODUCT_ID);
|
||||
assert.equal(result[0].productId, PRODUCT_ID);
|
||||
assert.equal(result[0].uid, client.uid);
|
||||
});
|
||||
|
||||
|
@ -303,7 +303,7 @@ describe('remote subscriptions:', function() {
|
|||
assert.lengthOf(result, 1);
|
||||
assert.isAbove(result[0].createdAt, Date.now() - 1000);
|
||||
assert.isNull(result[0].cancelledAt);
|
||||
assert.equal(result[0].productName, PRODUCT_ID);
|
||||
assert.equal(result[0].productId, PRODUCT_ID);
|
||||
assert.equal(result[0].uid, client.uid);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
FROM node:10-alpine
|
||||
|
||||
RUN npm install -g npm@6 && rm -rf ~app/.npm /tmp/*
|
||||
|
||||
RUN addgroup -g 10001 app && \
|
||||
adduser -D -G app -h /app -u 10001 app
|
||||
|
||||
|
@ -14,33 +12,30 @@ ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
|
|||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
RUN chown app:app /app
|
||||
USER app
|
||||
|
||||
COPY fxa-content-server/npm-shrinkwrap.json npm-shrinkwrap.json
|
||||
COPY fxa-content-server/package.json package.json
|
||||
COPY fxa-content-server/scripts/download_l10n.sh scripts/download_l10n.sh
|
||||
COPY --chown=app:app fxa-content-server/npm-shrinkwrap.json npm-shrinkwrap.json
|
||||
COPY --chown=app:app fxa-content-server/package.json package.json
|
||||
COPY --chown=app:app fxa-content-server/scripts/download_l10n.sh scripts/download_l10n.sh
|
||||
|
||||
RUN npm install --production --unsafe-perm && rm -rf ~/.cache ~/.npm /tmp/*
|
||||
RUN npm install --production && rm -rf ~/.cache ~/.npm /tmp/*
|
||||
|
||||
COPY fxa-content-server /app
|
||||
COPY --chown=app:app fxa-content-server /app
|
||||
|
||||
COPY ["fxa-geodb", "../fxa-geodb/"]
|
||||
COPY --chown=app:app ["fxa-geodb", "../fxa-geodb/"]
|
||||
WORKDIR /fxa-geodb
|
||||
USER root
|
||||
RUN chown -R app:app /fxa-geodb
|
||||
USER app
|
||||
RUN npm ci
|
||||
|
||||
COPY ["fxa-shared", "../fxa-shared/"]
|
||||
COPY --chown=app:app ["fxa-shared", "../fxa-shared/"]
|
||||
WORKDIR /fxa-shared
|
||||
USER root
|
||||
RUN chown -R app:app /fxa-shared
|
||||
USER app
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
||||
WORKDIR /app
|
||||
USER root
|
||||
RUN chown -R app:app /app
|
||||
USER app
|
||||
RUN npm run build-production --unsafe-perm
|
||||
RUN npm run build-production
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg height="2in" viewBox="0 0 216 144" width="3in" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><radialGradient id="a" cx="133.87" cy="114.93" gradientUnits="userSpaceOnUse" r="128.97"><stop offset=".4" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".58" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".77" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".96" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><radialGradient id="b" cx="89.36" cy="94.91" gradientUnits="userSpaceOnUse" r="102.61"><stop offset=".26" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".4" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".55" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".69" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset=".72" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><radialGradient id="c" cx="75.77" cy="203.9" gradientUnits="userSpaceOnUse" r="150.75"><stop offset=".27" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".46" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".66" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".86" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset=".9" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="105.4" x2="182.03" y1="76.22" y2="-.41"><stop offset=".43" stop-color="#00b3f4"/><stop offset=".61" stop-color="#00bbf6"/><stop offset=".89" stop-color="#00d2fc"/><stop offset="1" stop-color="#0df"/></linearGradient><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="49.63" x2="109.11" y1="105.11" y2="45.64"><stop offset="0" stop-color="#c689ff"/><stop offset="1" stop-color="#d74cf0"/></linearGradient><linearGradient id="f" gradientUnits="userSpaceOnUse" x1="19.43" x2="120.37" y1="141.35" y2="40.41"><stop offset=".22" stop-color="#b833e1"/><stop offset=".91" stop-color="#ff4f5e"/></linearGradient><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="62.25" x2="181.48" y1="169.32" y2="50.09"><stop offset=".28" stop-color="#7542e5"/><stop offset=".42" stop-color="#824deb"/><stop offset=".79" stop-color="#a067fa"/><stop offset="1" stop-color="#ab71ff"/></linearGradient><circle cx="106.58" cy="72" fill="url(#a)" r="67.79"/><path d="m74.68 36.27a9.51 9.51 0 0 1 -9.47 9.51h-38.07a7.27 7.27 0 0 1 0-14.54 7.15 7.15 0 0 1 1.74.22 7.82 7.82 0 0 1 -.2-1.69 7.5 7.5 0 0 1 7.51-7.49 7.42 7.42 0 0 1 4.23 1.32 12.6 12.6 0 0 1 24.79 3.16 9.51 9.51 0 0 1 9.47 9.51z" fill="url(#b)"/><path d="m193.59 105.85a10.8 10.8 0 0 0 -2.62.32 11.09 11.09 0 0 0 .29-2.53 11.31 11.31 0 0 0 -17.69-9.32 19 19 0 0 0 -37.36 4.78 14.27 14.27 0 1 0 0 28.54h57.38a10.9 10.9 0 1 0 0-21.79z" fill="url(#c)"/><rect fill="url(#d)" height="61.41" rx="5.4" width="75.77" x="106.31" y="6.71"/><path d="m172.01 32.2h4.99v9.99h-4.99z" fill="#0df"/><path d="m149.54 30.15a6.26 6.26 0 0 0 -8.85 0l-1.26 1.26-1.26-1.26a6.26 6.26 0 0 0 -8.85 8.85l9 9a1.51 1.51 0 0 0 1.08.45 1.53 1.53 0 0 0 1.08-.45l9-9a6.26 6.26 0 0 0 .06-8.85z" fill="#f9f9fa"/><path d="m66.08 101.22h29.85v8.65h-29.85z" fill="#ff848b"/><path d="m43.55 51.72h71.63v47.31h-71.63z" fill="url(#e)"/><path d="m123 103.81h-2v-53.76a3.86 3.86 0 0 0 -3.86-3.86h-75.49a3.86 3.86 0 0 0 -3.86 3.86v53.76h-1.95a1.92 1.92 0 0 0 -1.93 1.92v3.9a1.92 1.92 0 0 0 1.93 1.93h87.16a1.93 1.93 0 0 0 1.93-1.93v-3.9a1.93 1.93 0 0 0 -1.93-1.92zm-35.82 3.87h-15.52v-3.87h15.49z" fill="url(#f)"/><path d="m89.52 67.33a6.28 6.28 0 0 0 -8.85 0l-1.27 1.26-1.26-1.26a6.26 6.26 0 0 0 -8.85 0 6.26 6.26 0 0 0 0 8.85l9 9a1.51 1.51 0 0 0 2.15 0l9-9a6.26 6.26 0 0 0 .08-8.85z" fill="#f9f9fa"/><rect fill="url(#g)" height="62.76" rx="5.4" width="41.61" x="104.85" y="74.54"/><path d="m121.72 127.06h7.75v3.87h-7.75z" fill="#ab71ff"/><path d="m135.7 93.67a6.28 6.28 0 0 0 -8.85 0l-1.26 1.27-1.26-1.26a6.26 6.26 0 0 0 -8.85 8.85l9 9a1.53 1.53 0 0 0 1.08.45 1.51 1.51 0 0 0 1.08-.45l9-9a6.26 6.26 0 0 0 .06-8.86z" fill="#f9f9fa"/></svg>
|
||||
<svg height="2in" width="3in" viewBox="0 0 216 144" xmlns="http://www.w3.org/2000/svg"><style>@keyframes beat{0%,60%,90%,to{transform:scale(1)}25%{transform:scale(.8)}80%{transform:scale(.9)}}.heart{animation:beat 1.5s infinite}</style><radialGradient id="a" cx="133.87" cy="114.93" gradientUnits="userSpaceOnUse" r="128.97"><stop offset=".4" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".58" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".77" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".96" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><radialGradient id="b" cx="89.36" cy="94.91" gradientUnits="userSpaceOnUse" r="102.61"><stop offset=".26" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".4" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".55" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".69" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset=".72" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><radialGradient id="c" cx="75.77" cy="203.9" gradientUnits="userSpaceOnUse" r="150.75"><stop offset=".27" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".46" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".66" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".86" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset=".9" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="105.4" x2="182.03" y1="76.22" y2="-.41"><stop offset=".43" stop-color="#00b3f4"/><stop offset=".61" stop-color="#00bbf6"/><stop offset=".89" stop-color="#00d2fc"/><stop offset="1" stop-color="#0df"/></linearGradient><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="49.63" x2="109.11" y1="105.11" y2="45.64"><stop offset="0" stop-color="#c689ff"/><stop offset="1" stop-color="#d74cf0"/></linearGradient><linearGradient id="f" gradientUnits="userSpaceOnUse" x1="19.43" x2="120.37" y1="141.35" y2="40.41"><stop offset=".22" stop-color="#b833e1"/><stop offset=".91" stop-color="#ff4f5e"/></linearGradient><linearGradient id="g" gradientUnits="userSpaceOnUse" x1="62.25" x2="181.48" y1="169.32" y2="50.09"><stop offset=".28" stop-color="#7542e5"/><stop offset=".42" stop-color="#824deb"/><stop offset=".79" stop-color="#a067fa"/><stop offset="1" stop-color="#ab71ff"/></linearGradient><circle cx="106.58" cy="72" fill="url(#a)" r="67.79"/><path d="M74.68 36.27a9.51 9.51 0 01-9.47 9.51H27.14a7.27 7.27 0 010-14.54 7.15 7.15 0 011.74.22 7.82 7.82 0 01-.2-1.69 7.5 7.5 0 017.51-7.49 7.42 7.42 0 014.23 1.32 12.6 12.6 0 0124.79 3.16 9.51 9.51 0 019.47 9.51z" fill="url(#b)"/><path d="M193.59 105.85a10.8 10.8 0 00-2.62.32 11.09 11.09 0 00.29-2.53 11.31 11.31 0 00-17.69-9.32 19 19 0 00-37.36 4.78 14.27 14.27 0 100 28.54h57.38a10.9 10.9 0 100-21.79z" fill="url(#c)"/><rect fill="url(#d)" height="61.41" rx="5.4" width="75.77" x="106.31" y="6.71"/><path d="M172.01 32.2H177v9.99h-4.99z" fill="#0df"/><path class="heart" d="M149.54 30.15a6.26 6.26 0 00-8.85 0l-1.26 1.26-1.26-1.26a6.26 6.26 0 00-8.85 8.85l9 9a1.51 1.51 0 001.08.45 1.53 1.53 0 001.08-.45l9-9a6.26 6.26 0 00.06-8.85z" fill="#f9f9fa" style="transform-origin:65% 25%"/><path d="M66.08 101.22h29.85v8.65H66.08z" fill="#ff848b"/><path d="M43.55 51.72h71.63v47.31H43.55z" fill="url(#e)"/><path d="M123 103.81h-2V50.05a3.86 3.86 0 00-3.86-3.86H41.65a3.86 3.86 0 00-3.86 3.86v53.76h-1.95a1.92 1.92 0 00-1.93 1.92v3.9a1.92 1.92 0 001.93 1.93H123a1.93 1.93 0 001.93-1.93v-3.9a1.93 1.93 0 00-1.93-1.92zm-35.82 3.87H71.66v-3.87h15.49z" fill="url(#f)"/><path class="heart" d="M89.52 67.33a6.28 6.28 0 00-8.85 0l-1.27 1.26-1.26-1.26a6.26 6.26 0 00-8.85 0 6.26 6.26 0 000 8.85l9 9a1.51 1.51 0 002.15 0l9-9a6.26 6.26 0 00.08-8.85z" fill="#f9f9fa" style="transform-origin:37% 52%"/><rect fill="url(#g)" height="62.76" rx="5.4" width="41.61" x="104.85" y="74.54"/><path d="M121.72 127.06h7.75v3.87h-7.75z" fill="#ab71ff"/><path class="heart" d="M135.7 93.67a6.28 6.28 0 00-8.85 0l-1.26 1.27-1.26-1.26a6.26 6.26 0 00-8.85 8.85l9 9a1.53 1.53 0 001.08.45 1.51 1.51 0 001.08-.45l9-9a6.26 6.26 0 00.06-8.86z" fill="#f9f9fa" style="transform-origin:58% 69%"/></svg>
|
До Ширина: | Высота: | Размер: 3.9 KiB После Ширина: | Высота: | Размер: 4.1 KiB |
|
@ -1 +1 @@
|
|||
<svg height="2in" viewBox="0 0 216 144" width="3in" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><radialGradient id="a" cx="85.91" cy="159.8" gradientUnits="userSpaceOnUse" r="102.61"><stop offset=".26" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".4" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".55" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".69" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset=".72" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><radialGradient id="b" cx="72.32" cy="144.71" gradientUnits="userSpaceOnUse" r="150.75"><stop offset=".27" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".46" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".66" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".86" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset=".9" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><radialGradient id="c" cx="129.21" cy="117.26" gradientTransform="matrix(1 0 0 .7 0 35.37)" gradientUnits="userSpaceOnUse" r="112.67"><stop offset=".4" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".58" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".77" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".96" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="67.44" x2="126.91" y1="85.38" y2="25.9"><stop offset="0" stop-color="#c689ff"/><stop offset="1" stop-color="#d74cf0"/></linearGradient><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="37.24" x2="138.18" y1="121.61" y2="20.67"><stop offset=".22" stop-color="#b833e1"/><stop offset=".91" stop-color="#ff4f5e"/></linearGradient><linearGradient id="f" gradientUnits="userSpaceOnUse" x1="80.06" x2="199.29" y1="149.59" y2="30.36"><stop offset=".28" stop-color="#7542e5"/><stop offset=".42" stop-color="#824deb"/><stop offset=".79" stop-color="#a067fa"/><stop offset="1" stop-color="#ab71ff"/></linearGradient><path d="m71.24 101.17a9.5 9.5 0 0 1 -9.47 9.5h-38.08a7.27 7.27 0 0 1 0-14.53 7.17 7.17 0 0 1 1.75.22 7.3 7.3 0 0 1 -.2-1.69 7.49 7.49 0 0 1 7.5-7.49 7.41 7.41 0 0 1 4.26 1.31 12.61 12.61 0 0 1 24.8 3.17 9.5 9.5 0 0 1 9.44 9.51z" fill="url(#a)"/><path d="m190.14 46.66a10.72 10.72 0 0 0 -2.61.32 11.15 11.15 0 0 0 .29-2.53 11.31 11.31 0 0 0 -17.69-9.32 19 19 0 0 0 -37.37 4.78 14.27 14.27 0 1 0 0 28.53h57.39a10.89 10.89 0 1 0 0-21.78z" fill="url(#b)"/><path d="m24.87 72.66a22.54 22.54 0 0 1 22.45-22.66l120.77-.48a22.53 22.53 0 1 1 .18 45.06l-120.77.53a22.54 22.54 0 0 1 -22.63-22.45z" fill="url(#c)"/><path d="m83.89 81.48h29.85v8.65h-29.85z" fill="#ff848b"/><path d="m61.36 31.98h71.63v47.31h-71.63z" fill="url(#d)"/><path d="m140.78 84.07h-1.95v-53.76a3.86 3.86 0 0 0 -3.83-3.86h-75.54a3.86 3.86 0 0 0 -3.86 3.87v53.75h-2a1.92 1.92 0 0 0 -1.88 1.93v3.9a1.92 1.92 0 0 0 1.93 1.93h87.13a1.93 1.93 0 0 0 1.93-1.93v-3.9a1.93 1.93 0 0 0 -1.93-1.93zm-35.78 3.87h-15.53v-3.87h15.53z" fill="url(#e)"/><path d="m107.33 47.59a6.26 6.26 0 0 0 -8.85 0l-1.27 1.26-1.21-1.26a6.26 6.26 0 0 0 -8.85 0 6.26 6.26 0 0 0 0 8.85l9 9a1.49 1.49 0 0 0 1.07.45 1.51 1.51 0 0 0 1.08-.45l9-9a6.26 6.26 0 0 0 .03-8.85z" fill="#f9f9fa"/><rect fill="url(#f)" height="62.76" rx="5.4" width="41.61" x="122.66" y="54.8"/><path d="m139.53 107.32h7.75v3.87h-7.75z" fill="#ab71ff"/><path d="m153.51 73.93a6.26 6.26 0 0 0 -8.85 0l-1.26 1.26-1.26-1.26a6.26 6.26 0 1 0 -8.85 8.85l9 9a1.53 1.53 0 0 0 1.08.45 1.51 1.51 0 0 0 1.08-.45l9-9a6.26 6.26 0 0 0 .06-8.85z" fill="#f9f9fa"/></svg>
|
||||
<svg height="2in" width="3in" viewBox="0 0 216 144" xmlns="http://www.w3.org/2000/svg"><style>@keyframes beat{0%,60%,90%,to{transform:scale(1)}25%{transform:scale(.8)}80%{transform:scale(.9)}}.heart{animation:beat 1.5s infinite}</style><radialGradient id="a" cx="85.91" cy="159.8" gradientUnits="userSpaceOnUse" r="102.61"><stop offset=".26" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".4" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".55" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".69" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset=".72" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><radialGradient id="b" cx="72.32" cy="144.71" gradientUnits="userSpaceOnUse" r="150.75"><stop offset=".27" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".46" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".66" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".86" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset=".9" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><radialGradient id="c" cx="129.21" cy="117.26" gradientTransform="matrix(1 0 0 .7 0 35.37)" gradientUnits="userSpaceOnUse" r="112.67"><stop offset=".4" stop-color="#cdcdd4" stop-opacity="0"/><stop offset=".58" stop-color="#cdcdd4" stop-opacity=".02"/><stop offset=".77" stop-color="#cdcdd4" stop-opacity=".08"/><stop offset=".96" stop-color="#cdcdd4" stop-opacity=".18"/><stop offset="1" stop-color="#cdcdd4" stop-opacity=".2"/></radialGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="67.44" x2="126.91" y1="85.38" y2="25.9"><stop offset="0" stop-color="#c689ff"/><stop offset="1" stop-color="#d74cf0"/></linearGradient><linearGradient id="e" gradientUnits="userSpaceOnUse" x1="37.24" x2="138.18" y1="121.61" y2="20.67"><stop offset=".22" stop-color="#b833e1"/><stop offset=".91" stop-color="#ff4f5e"/></linearGradient><linearGradient id="f" gradientUnits="userSpaceOnUse" x1="80.06" x2="199.29" y1="149.59" y2="30.36"><stop offset=".28" stop-color="#7542e5"/><stop offset=".42" stop-color="#824deb"/><stop offset=".79" stop-color="#a067fa"/><stop offset="1" stop-color="#ab71ff"/></linearGradient><path d="M71.24 101.17a9.5 9.5 0 01-9.47 9.5H23.69a7.27 7.27 0 010-14.53 7.17 7.17 0 011.75.22 7.3 7.3 0 01-.2-1.69 7.49 7.49 0 017.5-7.49A7.41 7.41 0 0137 88.49a12.61 12.61 0 0124.8 3.17 9.5 9.5 0 019.44 9.51z" fill="url(#a)"/><path d="M190.14 46.66a10.72 10.72 0 00-2.61.32 11.15 11.15 0 00.29-2.53 11.31 11.31 0 00-17.69-9.32 19 19 0 00-37.37 4.78 14.27 14.27 0 100 28.53h57.39a10.89 10.89 0 100-21.78z" fill="url(#b)"/><path d="M24.87 72.66A22.54 22.54 0 0147.32 50l120.77-.48a22.53 22.53 0 11.18 45.06l-120.77.53a22.54 22.54 0 01-22.63-22.45z" fill="url(#c)"/><path d="M83.89 81.48h29.85v8.65H83.89z" fill="#ff848b"/><path d="M61.36 31.98h71.63v47.31H61.36z" fill="url(#d)"/><path d="M140.78 84.07h-1.95V30.31a3.86 3.86 0 00-3.83-3.86H59.46a3.86 3.86 0 00-3.86 3.87v53.75h-2A1.92 1.92 0 0051.72 86v3.9a1.92 1.92 0 001.93 1.93h87.13a1.93 1.93 0 001.93-1.93V86a1.93 1.93 0 00-1.93-1.93zM105 87.94H89.47v-3.87H105z" fill="url(#e)"/><path class="heart" d="M107.33 47.59a6.26 6.26 0 00-8.85 0l-1.27 1.26L96 47.59a6.26 6.26 0 00-8.85 0 6.26 6.26 0 000 8.85l9 9a1.49 1.49 0 001.07.45 1.51 1.51 0 001.08-.45l9-9a6.26 6.26 0 00.03-8.85z" fill="#f9f9fa" style="transform-origin:45% 38%"/><rect fill="url(#f)" height="62.76" rx="5.4" width="41.61" x="122.66" y="54.8"/><path d="M139.53 107.32h7.75v3.87h-7.75z" fill="#ab71ff"/><path class="heart" d="M153.51 73.93a6.26 6.26 0 00-8.85 0l-1.26 1.26-1.26-1.26a6.26 6.26 0 10-8.85 8.85l9 9a1.53 1.53 0 001.08.45 1.51 1.51 0 001.08-.45l9-9a6.26 6.26 0 00.06-8.85z" fill="#f9f9fa" style="transform-origin:66% 56%"/></svg>
|
До Ширина: | Высота: | Размер: 3.5 KiB После Ширина: | Высота: | Размер: 3.6 KiB |
До Ширина: | Высота: | Размер: 7.8 KiB После Ширина: | Высота: | Размер: 7.5 KiB |
До Ширина: | Высота: | Размер: 5.7 KiB После Ширина: | Высота: | Размер: 6.2 KiB |
После Ширина: | Высота: | Размер: 5.8 KiB |
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const popularEmailDomains = require('../../../../fxa-shared/email/popularDomains.json');
|
||||
const popularDomains = require('../../../../fxa-shared/dist/email/popularDomains.json');
|
||||
|
||||
/*eslint-disable sorting/sort-object-props*/
|
||||
module.exports = {
|
||||
|
@ -122,7 +122,7 @@ module.exports = {
|
|||
|
||||
// 20 most popular email domains, used for metrics. Matches the list
|
||||
// we use in the auth server, converted to a map for faster lookup.
|
||||
POPULAR_EMAIL_DOMAINS: popularEmailDomains.reduce((map, domain) => {
|
||||
POPULAR_EMAIL_DOMAINS: popularDomains.reduce((map, domain) => {
|
||||
map[domain] = true;
|
||||
return map;
|
||||
}, {}),
|
||||
|
|
|
@ -837,6 +837,22 @@ FxaClientWrapper.prototype = {
|
|||
*/
|
||||
sessions: createClientDelegate('sessions'),
|
||||
|
||||
/**
|
||||
* Get user's security events
|
||||
*
|
||||
* @param {String} sessionToken
|
||||
* @returns {Promise} resolves with response when complete.
|
||||
*/
|
||||
securityEvents: createClientDelegate('securityEvents'),
|
||||
|
||||
/**
|
||||
* Delete user's security events
|
||||
*
|
||||
* @param {String} sessionToken
|
||||
* @returns {Promise} resolves with empty response when complete.
|
||||
*/
|
||||
deleteSecurityEvents: createClientDelegate('deleteSecurityEvents'),
|
||||
|
||||
/**
|
||||
* Get user's attached clients
|
||||
*
|
||||
|
|
|
@ -38,6 +38,7 @@ import RecoveryCodesView from '../views/settings/recovery_codes';
|
|||
import RedirectAuthView from '../views/authorization';
|
||||
import ReportSignInView from '../views/report_sign_in';
|
||||
import ResetPasswordView from '../views/reset_password';
|
||||
import SecurityEvents from '../views/security_events';
|
||||
import SettingsView from '../views/settings';
|
||||
import SignInBouncedView from '../views/sign_in_bounced';
|
||||
import SignInPasswordView from '../views/sign_in_password';
|
||||
|
@ -165,6 +166,7 @@ const Router = Backbone.Router.extend({
|
|||
'secondary_email_verified(/)': createViewHandler(ReadyView, {
|
||||
type: VerificationReasons.SECONDARY_EMAIL_VERIFIED,
|
||||
}),
|
||||
'security_events(/)': createViewHandler(SecurityEvents),
|
||||
'settings(/)': createViewHandler(SettingsView),
|
||||
'settings/account_recovery(/)': createChildViewHandler(
|
||||
AccountRecoveryView,
|
||||
|
|
|
@ -458,6 +458,38 @@ const Account = Backbone.Model.extend(
|
|||
return this.pick(ALLOWED_PERSISTENT_KEYS);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the user's security events from the GET /securityEvents on the auth server
|
||||
*
|
||||
* @returns {Promise} resolves with security events
|
||||
*/
|
||||
securityEvents() {
|
||||
return Promise.resolve().then(() => {
|
||||
const sessionToken = this.get('sessionToken');
|
||||
if (!sessionToken) {
|
||||
throw AuthErrors.toError('INVALID_TOKEN');
|
||||
}
|
||||
|
||||
return this._fxaClient.securityEvents(sessionToken);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete user's security events from the DELETE /securityEvents on the auth server
|
||||
*
|
||||
* @returns {Promise} resolves with empty response if succeeded
|
||||
*/
|
||||
deleteSecurityEvents() {
|
||||
return Promise.resolve().then(() => {
|
||||
const sessionToken = this.get('sessionToken');
|
||||
if (!sessionToken) {
|
||||
throw AuthErrors.toError('INVALID_TOKEN');
|
||||
}
|
||||
|
||||
return this._fxaClient.deleteSecurityEvents(sessionToken);
|
||||
});
|
||||
},
|
||||
|
||||
setProfileImage(profileImage) {
|
||||
this.set({
|
||||
profileImageId: profileImage.get('id'),
|
||||
|
|
|
@ -149,6 +149,11 @@ const BaseAuthenticationBroker = Backbone.Model.extend({
|
|||
this._isForceAuth = this._isForceAuthUrl();
|
||||
this.importSearchParamsUsingSchema(QUERY_PARAMETER_SCHEMA, AuthErrors);
|
||||
|
||||
this.setCapability(
|
||||
'showSecurityEvents',
|
||||
!!this.getSearchParam('security_events')
|
||||
);
|
||||
|
||||
if (this.hasCapability('fxaStatus')) {
|
||||
return this._fetchFxaStatus({
|
||||
isPairing,
|
||||
|
@ -572,6 +577,10 @@ const BaseAuthenticationBroker = Backbone.Model.extend({
|
|||
* Is signup supported? the fx_ios_v1 broker can disable it.
|
||||
*/
|
||||
signup: true,
|
||||
/**
|
||||
* security events will be shown with `&security_events=true` in the url
|
||||
*/
|
||||
showSecurityEvents: false,
|
||||
/**
|
||||
* Does this environment support pairing?
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* Security events information
|
||||
*/
|
||||
|
||||
import Backbone from 'backbone';
|
||||
|
||||
const SecurityEvent = Backbone.Model.extend({
|
||||
defaults: {
|
||||
name: null,
|
||||
verified: false,
|
||||
createdAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
export default SecurityEvent;
|
|
@ -0,0 +1,26 @@
|
|||
<h1 id="recent-activity-header">{{#t}}Recent Activity{{/t}}</h1>
|
||||
|
||||
<h2>{{#t}}Security Events{{/t}}</h2>
|
||||
<table id="security-events-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{#t}}Event Name{{/t}}</th>
|
||||
<th>{{#t}}Verified{{/t}}</th>
|
||||
<th>{{#t}}Created At{{/t}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="security-events">
|
||||
{{#securityEvents}}
|
||||
<tr class="security-event">
|
||||
<td class="event-name">{{name}}</td>
|
||||
<td>{{verified}}</td>
|
||||
<td>{{createdAt}}</td>
|
||||
</tr>
|
||||
{{/securityEvents}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<button id="delete-events">{{#t}}Delete events{{/t}}</button>
|
||||
|
||||
<footer id="legal-footer"><a class="terms" href="/settings?security_events=true">{{#t}}Back to settings{{/t}}</a></footer>
|
|
@ -30,4 +30,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<footer id="legal-footer"><a class="terms" href="/legal/terms">{{#t}}Terms of Service{{/t}}</a><a class="privacy" href="/legal/privacy">{{#t}}Privacy Notice{{/t}}</a></footer>
|
||||
<footer id="legal-footer">
|
||||
{{#securityEventsVisible}}
|
||||
<a id="recent-activity-link" class="terms" href="/security_events">{{#t}}Recent Activity{{/t}}</a>
|
||||
{{/securityEventsVisible}}
|
||||
<a class="terms" href="/legal/terms">{{#t}}Terms of Service{{/t}}</a>
|
||||
<a class="privacy" href="/legal/privacy">{{#t}}Privacy Notice{{/t}}</a>
|
||||
</footer>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import 'modal';
|
||||
import BaseView from './base';
|
||||
import SecurityEvent from '../../scripts/models/security-events';
|
||||
import Template from 'templates/security_events.mustache';
|
||||
|
||||
let account;
|
||||
|
||||
const View = BaseView.extend({
|
||||
template: Template,
|
||||
className: 'security-events',
|
||||
viewName: 'security_events',
|
||||
|
||||
mustVerify: true,
|
||||
|
||||
events: {
|
||||
'click #delete-events': '_deleteSecurityEvents',
|
||||
},
|
||||
|
||||
beforeRender() {
|
||||
account = this.getSignedInAccount();
|
||||
if (!account) {
|
||||
this.navigate('/signin');
|
||||
}
|
||||
|
||||
return this._fetchSecurityEvents();
|
||||
},
|
||||
|
||||
setInitialContext(context) {
|
||||
context.set({
|
||||
securityEvents: this._securityEvents,
|
||||
});
|
||||
},
|
||||
|
||||
_fetchSecurityEvents() {
|
||||
return account.securityEvents().then(events => {
|
||||
this._securityEvents = events.map(event => {
|
||||
event.createdAt = formatDate(new Date(event.createdAt));
|
||||
|
||||
return new SecurityEvent(event).toJSON();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_deleteSecurityEvents() {
|
||||
return account.deleteSecurityEvents().then(() => {
|
||||
this._securityEvents = [];
|
||||
return this.render();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function formatDate(dateObj) {
|
||||
const date = dateObj.toDateString();
|
||||
let time = dateObj.toTimeString();
|
||||
const indexOfFirstSpace = time.indexOf(' ');
|
||||
time = time.substring(0, indexOfFirstSpace);
|
||||
|
||||
return `${date} ${time}`;
|
||||
}
|
||||
|
||||
export default View;
|
|
@ -90,6 +90,7 @@ const View = BaseView.extend({
|
|||
context.set({
|
||||
ccExpired: !!this._ccExpired,
|
||||
escapedCcExpiredLinkAttrs: `href="/subscriptions" class="alert-link"`,
|
||||
securityEventsVisible: this.displaySecurityEvents(),
|
||||
showSignOut: !account.isFromSync(),
|
||||
unsafeHeaderHTML: this._getHeaderHTML(account),
|
||||
});
|
||||
|
@ -146,6 +147,7 @@ const View = BaseView.extend({
|
|||
|
||||
afterRender() {
|
||||
const account = this.getSignedInAccount();
|
||||
|
||||
this.listenTo(account, 'change:displayName', this._onAccountUpdate);
|
||||
this.listenTo(account, 'change:email', this._onAccountUpdate);
|
||||
|
||||
|
@ -288,6 +290,14 @@ const View = BaseView.extend({
|
|||
}, this.SUCCESS_MESSAGE_DELAY_MS);
|
||||
return BaseView.prototype.unsafeDisplaySuccess.apply(this, arguments);
|
||||
},
|
||||
|
||||
displaySecurityEvents() {
|
||||
if (this.broker.hasCapability('showSecurityEvents')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
Cocktail.mixin(
|
||||
|
|
|
@ -19,3 +19,4 @@
|
|||
@import 'modules/marketing-ios';
|
||||
@import 'modules/support';
|
||||
@import 'modules/chosen';
|
||||
@import 'modules/security-events';
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
html {
|
||||
background-color: $html-background-color;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border: solid 1px #000;
|
||||
font-size: 1.2rem;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
|
@ -1604,6 +1604,53 @@ describe('lib/fxa-client', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('securityEvents', () => {
|
||||
it('delegates to the fxa-js-client', () => {
|
||||
const events = [
|
||||
{
|
||||
name: 'account.login',
|
||||
verified: 1,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
{
|
||||
name: 'account.create',
|
||||
verified: 1,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
];
|
||||
|
||||
sinon.stub(realClient, 'securityEvents').callsFake(() => {
|
||||
return Promise.resolve(events);
|
||||
});
|
||||
|
||||
return client.securityEvents('sessionToken').then(res => {
|
||||
assert.isTrue(realClient.securityEvents.calledWith('sessionToken'));
|
||||
assert.isTrue(realClient.securityEvents.calledOnce);
|
||||
|
||||
assert.equal(res.length, 2);
|
||||
assert.deepEqual(res[0], events[0]);
|
||||
assert.deepEqual(res[1], events[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSecurityEvents', () => {
|
||||
it('delegates to the fxa-js-client', () => {
|
||||
sinon.stub(realClient, 'deleteSecurityEvents').callsFake(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
return client.deleteSecurityEvents('sessionToken').then(res => {
|
||||
assert.isTrue(
|
||||
realClient.deleteSecurityEvents.calledWith('sessionToken')
|
||||
);
|
||||
assert.isTrue(realClient.deleteSecurityEvents.calledOnce);
|
||||
|
||||
assert.deepEqual(res, {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('smsStatus', () => {
|
||||
it('delegates to the fxa-js-client', () => {
|
||||
sinon.stub(realClient, 'smsStatus').callsFake(() =>
|
||||
|
|
|
@ -3020,4 +3020,49 @@ describe('models/account', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('securityEvents', () => {
|
||||
const events = [
|
||||
{
|
||||
name: 'account.login',
|
||||
verified: 1,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
{
|
||||
name: 'account.create',
|
||||
verified: 1,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
];
|
||||
|
||||
it('gets the security events', () => {
|
||||
account.set('sessionToken', SESSION_TOKEN);
|
||||
sinon.stub(fxaClient, 'securityEvents').callsFake(() => {
|
||||
return Promise.resolve(events);
|
||||
});
|
||||
|
||||
return account.securityEvents().then(res => {
|
||||
assert.isTrue(fxaClient.securityEvents.calledOnce);
|
||||
assert.isTrue(fxaClient.securityEvents.calledWith(SESSION_TOKEN));
|
||||
|
||||
assert.equal(res.length, 2);
|
||||
assert.deepEqual(res, events);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSecurityEvents', () => {
|
||||
it('deletes the security events', () => {
|
||||
account.set('sessionToken', SESSION_TOKEN);
|
||||
sinon.stub(fxaClient, 'deleteSecurityEvents').callsFake(() => {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
return account.deleteSecurityEvents().then(res => {
|
||||
assert.isTrue(fxaClient.deleteSecurityEvents.calledOnce);
|
||||
assert.isTrue(fxaClient.deleteSecurityEvents.calledWith(SESSION_TOKEN));
|
||||
assert.deepEqual(res, {});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { assert } from 'chai';
|
||||
import SecurityEvent from 'models/security-events';
|
||||
|
||||
describe('models/security-events', () => {
|
||||
let securityEvent;
|
||||
const events = [
|
||||
{
|
||||
name: 'account.login',
|
||||
verified: true,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
{
|
||||
name: 'account.create',
|
||||
verified: false,
|
||||
createdAt: new Date().getTime(),
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
securityEvent = events.map(event => {
|
||||
return new SecurityEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('correctly sets model properties', () => {
|
||||
assert.deepEqual(securityEvent[0].attributes, events[0]);
|
||||
assert.deepEqual(securityEvent[1].attributes, events[1]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,7 @@ import _ from 'underscore';
|
|||
import { assert } from 'chai';
|
||||
import AuthErrors from 'lib/auth-errors';
|
||||
import BaseView from 'views/base';
|
||||
import Broker from 'models/auth_brokers/base';
|
||||
import Cocktail from 'cocktail';
|
||||
import CommunicationPreferencesView from 'views/settings/communication_preferences';
|
||||
import SubscriptionView from 'views/settings/subscription';
|
||||
|
@ -35,6 +36,7 @@ Cocktail.mixin(SettingsPanelView, SettingsPanelMixin);
|
|||
|
||||
describe('views/settings', function() {
|
||||
var account;
|
||||
var broker;
|
||||
var experimentGroupingRules;
|
||||
var formPrefill;
|
||||
var initialChildView;
|
||||
|
@ -58,7 +60,9 @@ describe('views/settings', function() {
|
|||
|
||||
function createSettingsView() {
|
||||
subPanelRenderSpy = sinon.spy(() => Promise.resolve());
|
||||
broker = new Broker();
|
||||
view = new View({
|
||||
broker: broker,
|
||||
childView: initialChildView,
|
||||
config: {
|
||||
lang: 'en',
|
||||
|
|