Merge branch 'master' into train-144-merge

This commit is contained in:
Phil Booth 2019-08-27 20:57:44 +01:00
Родитель 19bdef698b a72c3e98d6
Коммит 6591843073
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 36FBB106F9C32516
157 изменённых файлов: 8190 добавлений и 6114 удалений

Просмотреть файл

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

6
packages/fortress/.gitignore поставляемый Normal file
Просмотреть файл

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

15
packages/fortress/README.md Executable file
Просмотреть файл

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

23
packages/fortress/bower.json Executable file
Просмотреть файл

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

12
packages/fortress/config.js Executable file
Просмотреть файл

@ -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;

5
packages/fortress/config.json Executable file
Просмотреть файл

@ -0,0 +1,5 @@
{
"client_id": "dcdb5ae7add825d2",
"client_secret": "b93ef8a8f3e553a430d7e5b904c6132b2722633af9f03128029201d24a97f2a8",
"redirect_uri": "http://127.0.0.1:9292/download"
}

2219
packages/fortress/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

43
packages/fortress/package.json Executable file
Просмотреть файл

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

46
packages/fortress/server.js Executable file
Просмотреть файл

@ -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&amp;</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>

Двоичные данные
packages/fortress/static/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Двоичные данные
packages/fortress/static/img/grad.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 147 B

Двоичные данные
packages/fortress/static/img/grad@2x.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 164 B

Двоичные данные
packages/fortress/static/img/growth.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.0 KiB

Двоичные данные
packages/fortress/static/img/growth@2x.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.5 KiB

Двоичные данные
packages/fortress/static/img/loading.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.5 KiB

Двоичные данные
packages/fortress/static/img/logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 873 B

Двоичные данные
packages/fortress/static/img/logo100.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.8 KiB

Двоичные данные
packages/fortress/static/img/logo@2x.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.5 KiB

Двоичные данные
packages/fortress/static/img/pro-header-bg.gif Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 80 KiB

Двоичные данные
packages/fortress/static/img/rocket.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

Двоичные данные
packages/fortress/static/img/rocket@2x.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.2 KiB

Двоичные данные
packages/fortress/static/img/transparent-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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&amp;</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;
}

125
packages/fxa-auth-db-mysql/npm-shrinkwrap.json сгенерированный
Просмотреть файл

@ -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] ||
[],

107
packages/fxa-auth-server/npm-shrinkwrap.json сгенерированный
Просмотреть файл

@ -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',

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше