Skip to content
This repository was archived by the owner on Sep 12, 2019. It is now read-only.

Commit 7703e4d

Browse files
author
sw-yx
committed
add identity event triggered functions
1 parent 08cfeaa commit 7703e4d

File tree

6 files changed

+186
-1
lines changed

6 files changed

+186
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module.exports = {
22
name: "hasura-event-triggered",
33
description:
4-
"Serverless function to process a Hasura event and fire off a GraphQL mutation with cleaned text data"
4+
"Hasura Cleaning: process a Hasura event and fire off a GraphQL mutation with processed text data"
55
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
name: "identity-signup",
3+
description:
4+
"Identity Signup: Triggered when a new Netlify Identity user confirms. Assigns roles and extra metadata"
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// note - this function MUST be named `identity-signup` to work
2+
// we do not yet offer local emulation of this functionality in Netlify Dev
3+
//
4+
// more:
5+
// https://www.netlify.com/blog/2019/02/21/the-role-of-roles-and-how-to-set-them-in-netlify-identity/
6+
// https://www.netlify.com/docs/functions/#identity-and-functions
7+
8+
exports.handler = async function(event, context) {
9+
const data = JSON.parse(event.body);
10+
const { user } = data;
11+
12+
const responseBody = {
13+
app_metadata: {
14+
roles: user.email.split("@")[1] === "trust-this-company.com" ? ["editor"] : ["visitor"];,
15+
my_user_info: "this is some user info"
16+
},
17+
user_metadata: {
18+
...user.user_metadata, // append current user metadata
19+
custom_data_from_function: "hurray this is some extra metadata"
20+
}
21+
};
22+
return {
23+
statusCode: 200,
24+
body: JSON.stringify(responseBody)
25+
};
26+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
name: "slack-rate-limit",
3+
description:
4+
"Slack Rate-limit: post to Slack, at most once an hour, using Neltify Identity metadata"
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "slack-rate-limit",
3+
"version": "1.0.0",
4+
"description": "netlify functions:create - post to Slack, at most once an hour, using Neltify Identity metadata",
5+
"main": "node-fetch.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"keywords": [
10+
"netlify",
11+
"serverless",
12+
"slack",
13+
"js"
14+
],
15+
"author": "Netlify",
16+
"license": "MIT",
17+
"dependencies": {
18+
"node-fetch": "^2.3.0"
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// code walkthrough: https://www.netlify.com/blog/2018/03/29/jamstack-architecture-on-netlify-how-identity-and-functions-work-together/#updating-user-data-with-the-identity-api
2+
// demo repo: https://github.com/biilmann/testing-slack-tutorial/tree/v3-one-message-an-hour
3+
// note: requires SLACK_WEBHOOK_URL environment variable
4+
const slackURL = process.env.SLACK_WEBHOOK_URL;
5+
const fetch = require("node-fetch");
6+
7+
class IdentityAPI {
8+
constructor(apiURL, token) {
9+
this.apiURL = apiURL;
10+
this.token = token;
11+
}
12+
13+
headers(headers = {}) {
14+
return {
15+
"Content-Type": "application/json",
16+
Authorization: `Bearer ${this.token}`,
17+
...headers
18+
};
19+
}
20+
21+
parseJsonResponse(response) {
22+
return response.json().then(json => {
23+
if (!response.ok) {
24+
return Promise.reject({ status: response.status, json });
25+
}
26+
27+
return json;
28+
});
29+
}
30+
31+
request(path, options = {}) {
32+
const headers = this.headers(options.headers || {});
33+
return fetch(this.apiURL + path, { ...options, headers }).then(response => {
34+
const contentType = response.headers.get("Content-Type");
35+
if (contentType && contentType.match(/json/)) {
36+
return this.parseJsonResponse(response);
37+
}
38+
39+
if (!response.ok) {
40+
return response.text().then(data => {
41+
return Promise.reject({ stauts: response.status, data });
42+
});
43+
}
44+
return response.text().then(data => {
45+
data;
46+
});
47+
});
48+
}
49+
}
50+
51+
/*
52+
Fetch a user from GoTrue via id
53+
*/
54+
function fetchUser(identity, id) {
55+
const api = new IdentityAPI(identity.url, identity.token);
56+
return api.request(`/admin/users/${id}`);
57+
}
58+
59+
/*
60+
Update the app_metadata of a user
61+
*/
62+
function updateUser(identity, user, app_metadata) {
63+
const api = new IdentityAPI(identity.url, identity.token);
64+
const new_app_metadata = { ...user.app_metadata, ...app_metadata };
65+
66+
return api.request(`/admin/users/${user.id}`, {
67+
method: "PUT",
68+
body: JSON.stringify({ app_metadata: new_app_metadata })
69+
});
70+
}
71+
72+
const oneHour = 60 * 60 * 1000;
73+
export function handler(event, context, callback) {
74+
if (event.httpMethod !== "POST") {
75+
return callback(null, {
76+
statusCode: 410,
77+
body: "Unsupported Request Method"
78+
});
79+
}
80+
81+
const claims = context.clientContext && context.clientContext.user;
82+
if (!claims) {
83+
return callback(null, {
84+
statusCode: 401,
85+
body: "You must be signed in to call this function"
86+
});
87+
}
88+
89+
fetchUser(context.clientContext.identity, claims.sub).then(user => {
90+
const lastMessage = new Date(
91+
user.app_metadata.last_message_at || 0
92+
).getTime();
93+
const cutOff = new Date().getTime() - oneHour;
94+
if (lastMessage > cutOff) {
95+
return callback(null, {
96+
statusCode: 401,
97+
body: "Only one message an hour allowed"
98+
});
99+
}
100+
101+
try {
102+
const payload = JSON.parse(event.body);
103+
104+
fetch(slackURL, {
105+
method: "POST",
106+
body: JSON.stringify({
107+
text: payload.text,
108+
attachments: [{ text: `From ${user.email}` }]
109+
})
110+
})
111+
.then(() =>
112+
updateUser(context.clientContext.identity, user, {
113+
last_message_at: new Date().getTime()
114+
})
115+
)
116+
.then(() => {
117+
callback(null, { statusCode: 204 });
118+
})
119+
.catch(err => {
120+
callback(null, {
121+
statusCode: 500,
122+
body: "Internal Server Error: " + e
123+
});
124+
});
125+
} catch (e) {
126+
callback(null, { statusCode: 500, body: "Internal Server Error: " + e });
127+
}
128+
});
129+
}

0 commit comments

Comments
 (0)