Skip to content

Commit bfaf1a7

Browse files
author
Nicolas Garnier
committed
Add new "delete-unused-accounts-cron" sample.
Change-Id: I882ac7a6292ca6348ffd8d155f2946303b6f5f92
1 parent 546dd16 commit bfaf1a7

File tree

7 files changed

+230
-2
lines changed

7 files changed

+230
-2
lines changed

Diff for: README.md

+6
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ Sends a Welcome email when a user signs-in for the first time.
131131

132132
Uses an Auth trigger.
133133

134+
### [Delete Inactive Users Accounts Cron](/delete-unused-accounts-cron)
135+
136+
Periodically deletes the accounts of users who have not signed in the last month.
137+
138+
Uses an HTTPS trigger.
139+
134140

135141
## Contributing
136142

Diff for: delete-unused-accounts-cron/README.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Periodically delete unused accounts.
2+
3+
This sample demonstrates how to delete the accounts of users who have not signed-in in the last month.
4+
5+
6+
## Functions Code
7+
8+
See file [functions/index.js](functions/index.js) for the code.
9+
10+
Firebase Functions does not naticaly supports cron jobs. We are working around this by executing the code as an HTTPS triggered function. Then simply use an external service to periodically "ping" the URL.
11+
12+
Here is a non-exhaustive list of external services for cron jobs:
13+
- https://www.setcronjob.com/
14+
- https://cron-job.org/
15+
- https://www.easycron.com/
16+
17+
The dependencies are listed in [functions/package.json](functions/package.json).
18+
19+
20+
## Trigger rules
21+
22+
The function triggers when the HTTP URL of the Function is requested.
23+
24+
25+
## Deploy and test
26+
27+
Set the `cron.key` Google Cloud environment variables to a randomly generated key, this will be used to authorize requests coming from the 3rd-party cron service. For this use:
28+
29+
```bash
30+
firebase env:set cron.key="YOUR_KEY"
31+
```
32+
33+
You can generate a random key, for instance, by running:
34+
35+
```bash
36+
npm install -g crypto
37+
node -e "console.log(require('crypto').randomBytes(20).toString('hex'))"
38+
```
39+
40+
To set up the sample:
41+
42+
- Create a Firebase Project using the Firebase Developer Console
43+
- Enable billing since Functions require billing.
44+
- Download the service accounts using the Firebase Developer Console at: **"Wheely" Icon > Project Settings > Service Accounts > Generate New Private Key** and save it as `./functions/service-accounts.json`.
45+
- Deploy your project using `firebase deploy`.
46+
- Open an account with a 3rd party cron service (e.g. www.setcronjob.com, cron-job.org, www.easycron.com ...) and setup a daily cron job to hit the URL (don't forget to change `YOUR_KEY`):
47+
48+
```
49+
https://us-central1-<project-id>.cloudfunctions.net/accountcleanup?key=YOUR_KEY
50+
```

Diff for: delete-unused-accounts-cron/firebase.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

Diff for: delete-unused-accounts-cron/functions/index.js

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Copyright 2016 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for t`he specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
'use strict';
17+
18+
const functions = require('firebase-functions');
19+
const firebaseAdmin = require('firebase-admin');
20+
const serviceAccount = require('./service-account.json');
21+
firebaseAdmin.initializeApp({
22+
credential: firebaseAdmin.credential.cert(serviceAccount),
23+
databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`
24+
});
25+
const google = require('googleapis');
26+
const rp = require('request-promise');
27+
const promisePool = require('es6-promise-pool');
28+
const PromisePool = promisePool.PromisePool;
29+
const MAX_CONCURRENT = 3;
30+
31+
/**
32+
* When requested this Function will delete every user accounts that has been inactive for 30 days.
33+
* The request needs to be authorized by passing a 'key' query parameter in the URL. This key must
34+
* match a key set as an environment variable using `firebase env:set cron.key="YOUR_KEY"`.
35+
*/
36+
exports.accountcleanup = functions.https().onRequest((req, res) => {
37+
const key = req.query.key;
38+
39+
// Exit if the keys don't match
40+
if (key !== functions.env.cron.key) {
41+
console.log('The key provided in the request does not match the key set in the environment. Check that', key,
42+
'matches the cron.key attribute in `firebase env:get`');
43+
return res.status(403).send('Security key does not match. Make sure your "key" URL query parameter matches the ' +
44+
'cron.key environment variable.');
45+
}
46+
47+
// We'll fetch all user details.
48+
getUsers().then(users => {
49+
// We'll use a pool so that we delete maximum `MAX_CONCURRENT` users in parallel.
50+
const promisePool = new PromisePool(() => {
51+
let user;
52+
// We search for users that have not signed in in the last 30 days.
53+
while (!user || user.metadata.lastSignedInAt.getTime() > Date.now() - 30 * 24 * 60 * 60 * 1000) {
54+
if (users.length === 0) {
55+
return null;
56+
}
57+
user = users.pop();
58+
}
59+
60+
// If an inactive user is found we delete it.
61+
return firebaseAdmin.auth().deleteUser(user.uid).then(() => {
62+
console.log('Deleted user account', user.uid, 'because of inactivity');
63+
}).catch(error => {
64+
console.error('Deletion of inactive user account', user.uid, 'failed:', error);
65+
});
66+
}, MAX_CONCURRENT);
67+
68+
return promisePool.start().then(() => {
69+
console.log('User cleanup finished');
70+
res.send('User cleanup finished');
71+
});
72+
});
73+
});
74+
75+
/**
76+
* Returns the list of all users. Including additional metadata such as last sign-in Date.
77+
*/
78+
function getUsers() {
79+
// Create a pool so that there is only `MAX_CONCURRENT` max parallel requests to fetch user details.
80+
return getUserIds().then(userIds => {
81+
const users = [];
82+
83+
const promisePool = new PromisePool(() => {
84+
if (userIds.length === 0) {
85+
return null;
86+
}
87+
const nextUserId = userIds.pop();
88+
return firebaseAdmin.auth().getUser(nextUserId).then(user => {
89+
users.push(user);
90+
});
91+
}, MAX_CONCURRENT);
92+
93+
return promisePool.start().then(() => users);
94+
});
95+
}
96+
97+
/**
98+
* Returns the list of all user Ids.
99+
*/
100+
function getUserIds(userIds = [], nextPageToken, accessToken) {
101+
return getAccessToken(accessToken).then(accessToken => {
102+
const options = {
103+
method: 'POST',
104+
uri: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/downloadAccount?fields=users/localId,nextPageToken&access_token=' + accessToken,
105+
body: {
106+
nextPageToken: nextPageToken,
107+
maxResults: 1000
108+
},
109+
json: true // Automatically stringifies the body to JSON
110+
};
111+
112+
return rp(options).then(resp => {
113+
if (!resp.users) {
114+
return userIds;
115+
}
116+
resp.users.forEach(user => {
117+
userIds.push(user.localId);
118+
});
119+
if (resp.nextPageToken) {
120+
return getUserIds(userIds, resp.nextPageToken, accessToken);
121+
}
122+
return userIds;
123+
});
124+
});
125+
}
126+
127+
/**
128+
* Returns an access token using the Service accounts credentials.
129+
*/
130+
function getAccessToken(accessToken) {
131+
if (accessToken) {
132+
return Promise.resolve(accessToken);
133+
}
134+
135+
const jwtClient = new google.auth.JWT(serviceAccount.client_email, null,
136+
serviceAccount.private_key, ['https://www.googleapis.com/auth/firebase'], null);
137+
138+
return new Promise((resolve, reject) => {
139+
jwtClient.authorize((error, token) => {
140+
if (error) {
141+
console.error('Error while fetching access token for Service accounts', error);
142+
return reject();
143+
}
144+
resolve(token.access_token);
145+
});
146+
});
147+
}

Diff for: delete-unused-accounts-cron/functions/package.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "fcm-notifications-functions",
3+
"description": "Send FCM notifications Firebase Functions sample",
4+
"dependencies": {
5+
"es6-promise-pool": "^2.4.4",
6+
"firebase": "^3.6",
7+
"firebase-admin": "^4.0.1",
8+
"firebase-functions": "https://storage.googleapis.com/firebase-preview-drop/node/firebase-functions/firebase-functions-preview.latest.tar.gz",
9+
"request": "^2.79.0",
10+
"request-promise": "^4.1.1",
11+
"request-promise-native": "^1.0.3"
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "service_account",
3+
"project_id": "functions-fcm",
4+
"private_key_id": "f49aa12fc8ce9ddc7c5a5f2e56bfb34a4b1376cd",
5+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQCe42jBELgCnq\n94iq74kLeyzwLGEsbfdVang7F5Lf76+BIhQ7LziVRmMNJcvwRz/UptGRc58v0xFz\n55a9hCRuRFN04glUOPS+lNzNtJN9cGSsZZV+36h6/nvEnnuJxHGXZtmTD8ymeaJs\nqL0RxAa6RSwNkweYV0mgCILqX9+qcrU494n3grWVgWop4HrV+IWzvZ9QMHb9K4w1\nwQoAFpiwD0B45abgPT68UbFWcy8wYoQgXwrvKc3n2z4RevfTi5EUmd76fd52PKBc\nAbLa/NcHB9iQ0+rR/u9zLAZ3Rm/1Wso+99hBCFMLHZHa+ujFI+q4Y+Fyw2SLgVgF\ni9iBCFvTAgMBAAECggEBAKCJmT6Ula6nRzGftOIbmEi407O+u3n6netXDtKi3V9b\nabaforcNOH/Q4izKJvcTNEmYNY4liAjyurwTUXqLl0VUCobeys4oaY0L+NvwZgRd\nkAKNHXDbjPrkmIPgvHpSkkmAP5PBlG4+3L029TfZakuhh14uQKUpbpJFHylXtJSl\nLVzWqVD0d3P1Z5KqHau8FLOtq+TwlL9tQjh1Ym6wqvdqMi4NHkdMCVIWtXyfI8xw\nGKaPlLMx1hYsRbbcnnxzzauZrgEOyT/q4Q1Hn7eENl1j0SVyTe01tJA2Odw8NkIu\npmyeMIztc/WqsOml+zGwgajw/EjJ6TabZnj13Bii9JkCgYEA8rvtG6wIGOZlQ3hJ\nbwip9LFIMZZiI0EDytnansAOVujIE7/UY2/wfyE4aOZgHBOWzbbUkZ2S4vQ0DPeg\nmQ6As1Kqo4g8zdXZjqgqVj4I83WNPFHTCrSXaK47p4YtCcHBy2M3Pu6t/1vQ5fMP\nFlo57M8Ex5MNDMGE21F4tAusNNUCgYEA22iVyqxLsL4W0CidOZ+1snxaxjtKDHt9\nBCSrs06itcnnhTSLBI55BHqI6stb+GHbZOaZ6/lSJjPKr+IFy5T1BzfgiMEcY6ee\nsxU+5lQxJ6D1JKkLdUtNppRKCyx0q2G7qscD7CpmBpXCKt/FnBqpqtjfBsqT1dhZ\nXoG/LczkQgcCgYASOAbA9/WXoNti6Ali+xx+kDvh9O6ixMN7G0Tse2/YGBrEWLah\nTAqaEC1Cul/mW5YLFLj5wQEAZeHuQzvboRoJ25+RLK2bqXxt17Nty7QySdVy/JVB\njXJ72fACT/DbdZ6NHIJOB+4pZ4PTbp3oSJdmbddm/2OQXIoTSBcuNF4VjQKBgFt+\nXGB8wr98NUUueonqCLnaU3wwgyt7X2GX7SXDl+RYwrvwcjw/MUXl1yyaCssj+3oz\nE2KswE3/8PixNxtzDU6qRW6hoLYJ0wr4xBcGas0MuM1F1OpfsYzSb6IDMs+43KpV\nfVRBRfRfBO4eDGiRUclV0IMjfMyDAJmBX3i45UKHAoGBAMyKh8ysZuZypcwuWXHn\ncUEv3YjciGzfhShVCyCLJ21IEr/Fpt4DwIO8lyaL1s1ez0q39QnTc/ZeHG6Q6trZ\nCpT+zExvENE2tRQFR/M4eEwsZM+dOYkywoM4SDmEzck+Nt9FtlkL+tR1DB+vT0/x\ns67Nlpbyq0AbxtptOSW7HWMp\n-----END PRIVATE KEY-----\n",
6+
"client_email": "[email protected]",
7+
"client_id": "115601641616008366344",
8+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
9+
"token_uri": "https://accounts.google.com/o/oauth2/token",
10+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-oz89s%40functions-fcm.iam.gserviceaccount.com"
12+
}

Diff for: message-translation/functions/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"dependencies": {
55
"firebase": "^3.6",
66
"firebase-functions": "https://storage.googleapis.com/firebase-preview-drop/node/firebase-functions/firebase-functions-preview.latest.tar.gz",
7-
"request-promise": "^2.0.0",
8-
"rsvp": "^3.1.0"
7+
"request-promise": "^2.0.0"
98
}
109
}

0 commit comments

Comments
 (0)