publish, archive, access keys and login updated

This commit is contained in:
Eslam A. Hefnawy 2018-06-13 16:33:11 +03:00
parent 10727c5d79
commit 396febb306
18 changed files with 242 additions and 535 deletions

View File

@ -133,6 +133,8 @@ class Service {
that.service = serverlessFile.service;
}
that.app = serverlessFile.app;
that.tenant = serverlessFile.tenant;
that.custom = serverlessFile.custom;
that.plugins = serverlessFile.plugins;
that.resources = serverlessFile.resources;

View File

@ -1,14 +0,0 @@
'use strict';
const forge = require('node-forge');
module.exports = (encryptedToken, key, iv) => {
const decipherToken = forge.cipher.createDecipher('AES-CBC', key);
decipherToken.start({ iv });
decipherToken.update(forge.util.createBuffer(forge.util.decode64(encryptedToken)));
const result = decipherToken.finish(); // check 'result' for true/false
if (!result) {
throw new Error(`Couldn't decrypt token: ${encryptedToken}`);
}
return decipherToken.output.toString();
};

View File

@ -1,23 +0,0 @@
'use strict';
const expect = require('chai').expect;
const forge = require('node-forge');
const decryptToken = require('./decryptToken');
const encryptAuthToken = (token, key, iv) => {
const cipher = forge.cipher.createCipher('AES-CBC', key);
cipher.start({ iv });
cipher.update(forge.util.createBuffer(token));
cipher.finish();
return forge.util.encode64(cipher.output.data);
};
describe('#decryptToken()', () => {
it('should encrypt a token with AWS CBC', () => {
const token = 'f3120811-8306-4d67-98e5-13fde2e490e4';
const key = forge.random.getBytesSync(16);
const iv = forge.random.getBytesSync(16);
const encryptedToken = encryptAuthToken(token, key, iv);
expect(decryptToken(encryptedToken, key, iv)).to.equal(token);
});
});

View File

@ -1,20 +0,0 @@
'use strict';
const gql = require('graphql-tag');
module.exports = (id, apolloQueryFn) =>
apolloQueryFn({
fetchPolicy: 'network-only',
query: gql`
query cliLoginById($id: String!) {
cliLoginById(id: $id) {
encryptedAccessToken
encryptedIdToken
encryptedRefreshToken
encryptedKey
encryptedIv
}
}
`,
variables: { id },
}).then(response => response.data);

View File

@ -1,32 +0,0 @@
'use strict';
const sinon = require('sinon');
const expect = require('chai').expect;
const gql = require('graphql-tag');
const getCliLoginById = require('./getCliLoginById');
describe('#getCliLoginById()', () => {
it('should query for the cliLoginById', () => {
const expectedParams = {
fetchPolicy: 'network-only',
query: gql`
query cliLoginById($id: String!) {
cliLoginById(id: $id) {
encryptedAccessToken
encryptedIdToken
encryptedRefreshToken
encryptedKey
encryptedIv
}
}
`,
variables: { id: 'abcd' },
};
const query = sinon.stub().resolves({ data: { cliLoginId: 'abcd' } });
return getCliLoginById('abcd', query).then(data => {
expect(data).to.deep.equal({ cliLoginId: 'abcd' });
expect(query.getCall(0).args[0]).to.deep.equal(expectedParams);
});
});
});

View File

@ -2,26 +2,9 @@
const BbPromise = require('bluebird');
const jwtDecode = require('jwt-decode');
const chalk = require('chalk');
const uuid = require('uuid');
const has = require('lodash/has');
const forge = require('node-forge');
const querystring = require('querystring');
const openBrowser = require('../../utils/openBrowser');
const platform = require('@serverless/platform-sdk');
const configUtils = require('../../utils/config');
const clearConsole = require('../../utils/clearConsole');
const userStats = require('../../utils/userStats');
const setConfig = require('../../utils/config').set;
const createApolloClient = require('../../utils/createApolloClient');
const decryptToken = require('./lib/decryptToken');
const getCliLoginById = require('./lib/getCliLoginById');
const config = {
PLATFORM_FRONTEND_BASE_URL: 'https://platform.serverless.com/',
GRAPHQL_ENDPOINT_URL: 'https://graphql.serverless.com/graphql',
};
const client = createApolloClient(config.GRAPHQL_ENDPOINT_URL);
class Login {
constructor(serverless, options) {
@ -41,136 +24,53 @@ class Login {
};
}
login() {
clearConsole();
this.serverless.cli.log('The Serverless login will open in your default browser...');
const configuration = configUtils.getConfig();
const frameworkId = configuration.frameworkId;
const cliLoginId = uuid.v4();
let fetchTries = 0;
forge.pki.rsa.generateKeyPair({ bits: 2048 }, (generateKeyErr, keypair) => {
if (generateKeyErr) {
this.serverless.cli.log(
'Sorry, failed initiating the authentication key. ',
'Please contact the Serverless team at hello@serverless.com'
);
throw generateKeyErr;
}
const publicKeyPem = forge.pki.publicKeyToPem(keypair.publicKey);
const encodedPublicKeyPem = forge.util.encode64(publicKeyPem);
this.serverless.cli.log('Opening browser...');
// Avoid camel casing since queries is going into to be part of the URL
const queries = querystring.stringify({
cli: 'v1',
'login-id': cliLoginId,
'public-key': encodedPublicKeyPem,
});
// open default browser
openBrowser(`${config.PLATFORM_FRONTEND_BASE_URL}login?${queries}`);
const fetchCliLogin = () => {
fetchTries += 1;
// indicating the user after a while that the CLI is still waiting
if (fetchTries >= 60) {
this.serverless.cli.log('Waiting for a successful authentication in the browser.');
fetchTries = 0;
}
getCliLoginById(cliLoginId, client.query)
.then(response => {
const hasAllToken =
has(response, ['cliLoginById', 'encryptedAccessToken']) &&
has(response, ['cliLoginById', 'encryptedIdToken']) &&
has(response, ['cliLoginById', 'encryptedRefreshToken']) &&
has(response, ['cliLoginById', 'encryptedKey']) &&
has(response, ['cliLoginById', 'encryptedIv']) &&
response.cliLoginById.encryptedAccessToken !== null &&
response.cliLoginById.encryptedIdToken !== null &&
response.cliLoginById.encryptedRefreshToken !== null &&
response.cliLoginById.encryptedKey !== null &&
response.cliLoginById.encryptedIv !== null;
if (!hasAllToken) {
// delay the requests for a bit
setTimeout(() => fetchCliLogin(), 500);
} else {
const key = keypair.privateKey.decrypt(
forge.util.decode64(response.cliLoginById.encryptedKey),
'RSA-OAEP'
);
const iv = keypair.privateKey.decrypt(
forge.util.decode64(response.cliLoginById.encryptedIv),
'RSA-OAEP'
);
const idToken = decryptToken(response.cliLoginById.encryptedIdToken, key, iv);
const accessToken = decryptToken(response.cliLoginById.encryptedAccessToken, key, iv);
const refreshToken = decryptToken(
response.cliLoginById.encryptedRefreshToken,
key,
iv
);
const decoded = jwtDecode(idToken);
this.serverless.cli.log('You are now logged in');
// because platform only support github
const id = decoded.tracking_id || decoded.sub;
const userConfig = {
userId: id,
frameworkId,
users: {},
};
// set user auth in global .serverlessrc file
userConfig.users[id] = {
userId: id,
name: decoded.name,
email: decoded.email,
auth: {
id_token: idToken,
access_token: accessToken,
refresh_token: refreshToken,
},
};
// update .serverlessrc
setConfig(userConfig);
// identify user for better onboarding
userStats
.identify({
id,
frameworkId,
email: decoded.email,
// unix timestamp
created_at: Math.round(+new Date(decoded.createdAt) / 1000),
trackingDisabled: configuration.trackingDisabled,
force: true,
})
.then(() => {
userStats
.track('user_loggedIn', {
id,
email: decoded.email,
force: true,
})
.then(() => {
// then exit process
process.exit(0);
});
});
}
})
.catch(() => {
this.serverless.cli.consoleLog(
chalk.red('Sorry, something went wrong. Please run "serverless login" again.')
);
throw new this.serverless.classes.Error(
'Failed to login due to an error. Please try again or contact hello@serverless.com'
);
});
return platform.login().then(data => {
const decoded = jwtDecode(data.idToken);
// because platform only support github
const id = decoded.tracking_id || decoded.sub;
const userConfig = {
userId: id,
frameworkId,
users: {},
};
// set user auth in global .serverlessrc file
userConfig.users[id] = {
userId: id,
name: decoded.name,
email: decoded.email,
username: decoded.nickname,
auth: configuration.users[id].auth,
dashboard: data,
};
fetchCliLogin();
// update .serverlessrc
configUtils.set(userConfig);
// identify user for better onboarding
userStats
.identify({
id,
frameworkId,
email: decoded.email,
// unix timestamp
created_at: Math.round(+new Date(decoded.createdAt) / 1000),
trackingDisabled: configuration.trackingDisabled,
force: true,
})
.then(() => {
userStats
.track('user_loggedIn', {
id,
email: decoded.email,
force: true,
});
});
this.serverless.cli.log('You are now logged in');
process.exit(0);
});
}
}

View File

@ -30,9 +30,9 @@ class Logout {
// that invalidate a refresh token in Auth0 (using the Auth0 Management API).
if (globalConfig && globalConfig.users && globalConfig.users[currentId]) {
if (globalConfig.users[currentId].auth) {
if (globalConfig.users[currentId].dashboard) {
// remove auth tokens from user
configUtils.set(`users.${currentId}.auth`, null);
configUtils.set(`users.${currentId}.dashboard`, null);
// log stat
userStats.track('user_loggedOut').then(() => {
this.serverless.cli.consoleLog('Successfully logged out.');

View File

@ -2,64 +2,21 @@
/* eslint-disable no-console */
const path = require('path');
const fs = require('fs');
const gql = require('graphql-tag');
const jwtDecode = require('jwt-decode');
const BbPromise = require('bluebird');
const fsExtra = require('../../utils/fs/fse');
const fetch = require('node-fetch');
const chalk = require('chalk');
const functionInfoUtils = require('../../utils/functionInfoUtils');
const createApolloClient = require('../../utils/createApolloClient');
const getAuthToken = require('../../utils/getAuthToken');
const selectServicePublish = require('../../utils/selectors/selectServicePublish');
// NOTE Needed for apollo to work
global.fetch = fetch;
const config = {
PLATFORM_FRONTEND_BASE_URL: 'https://platform.serverless.com/',
GRAPHQL_ENDPOINT_URL: 'https://graphql.serverless.com/graphql',
};
function addReadme(attributes, readmePath) {
if (fs.existsSync(readmePath)) {
const readmeContent = fsExtra.readFileSync(readmePath).toString('utf8');
// eslint-disable-next-line no-param-reassign
attributes.readme = readmeContent;
}
return attributes;
}
function fetchEndpoint(provider) {
return provider
.request(
'CloudFormation',
'describeStacks',
{ StackName: provider.naming.getStackName() },
{ useCache: true } // Use request cache
)
.then(result => {
let endpoint = null;
if (result) {
result.Stacks[0].Outputs
.filter(x => x.OutputKey.match(provider.naming.getServiceEndpointRegex()))
.forEach(x => {
endpoint = x.OutputValue;
});
}
return endpoint;
});
}
const crypto = require('crypto');
const _ = require('lodash');
const platform = require('@serverless/platform-sdk');
const getAccessKey = require('../../utils/getAccessKey');
class Platform {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options;
this.provider = this.serverless.getProvider('aws');
this.config = {
app: this.serverless.service.app,
tenant: this.serverless.service.tenant,
};
// NOTE for the time being we only track services published to AWS
if (this.provider) {
this.hooks = {
@ -69,141 +26,120 @@ class Platform {
}
}
archiveServiceRequest(name, client) {
return client.mutate({
mutation: gql`
mutation archiveService($name: String!) {
archiveService(name: $name) {
archived
publishService() {
return getAccessKey(this.config.tenant).then(accessKey => {
if (!accessKey || !this.config.app || !this.config.tenant) {
return BbPromise.resolve();
}
this.serverless.cli.log('Publishing service to Serverless Platform...');
const service = this.serverless.service;
const data = {
app: this.config.app,
tenant: this.config.tenant,
accessKey,
version: '0.1.0',
service: this.getServiceData(),
functions: [],
subscriptions: [],
};
Object.keys(service.functions).forEach(fn => {
const fnObj = _.omit(service.functions[fn], ['events']);
fnObj.functionId = this.serverless.service.getFunction(fn).name;
data.functions.push(fnObj);
this.serverless.service.getAllEventsInFunction(fn).forEach(event => {
const subscription = {
functionId: this.serverless.service.getFunction(fn).name,
};
// in case of sls custom event type...
if (typeof event === 'string') {
subscription.type = event;
subscription.subscriptionId = crypto.createHash('md5')
.update(`${subscription.functionId}-${event}`).digest('hex');
// in case of aws apigateway
} else if (Object.keys(event)[0] === 'http') {
subscription.type = 'aws.apigateway.http';
if (typeof event.http === 'string') {
// todo http shortcut
} else if (typeof event.http === 'object') {
subscription.method = event.http.method;
subscription.path = event.http.path;
subscription.subscriptionId = crypto.createHash('md5')
.update(JSON.stringify(subscription)).digest('hex');
}
}
}
`,
variables: { name },
// todo support aws events and http
data.subscriptions.push(subscription);
});
});
return new BbPromise((resolve, reject) => {
platform.publishService(data)
.then(() => {
this.serverless.cli
.log('Successfully published your service on the Serverless Platform');
resolve();
process.exit(0);
})
.catch(err => {
this.serverless.cli.log('Failed to published your service on the Serverless Platform');
reject(err.message);
});
});
});
}
getServiceData() {
const serviceData = {
name: this.serverless.service.service,
stage: this.serverless.processedInput.options.stage
|| this.serverless.service.provider.stage,
provider: this.serverless.service.provider,
};
if (this.serverless.service.serviceObject.description) {
serviceData.description = this.serverless.service.serviceObject.description;
}
if (this.serverless.service.serviceObject.license) {
serviceData.license = this.serverless.service.serviceObject.license;
}
if (this.serverless.service.serviceObject.bugs) {
serviceData.bugs = this.serverless.service.serviceObject.bugs;
}
if (this.serverless.service.serviceObject.repository) {
serviceData.repository = this.serverless.service.serviceObject.repository;
}
if (this.serverless.service.serviceObject.homepage) {
serviceData.homepage = this.serverless.service.serviceObject.homepage;
}
return serviceData;
}
archiveService() {
const authToken = this.getAuthToken();
const publishFlag = selectServicePublish(this.serverless.service);
if (!authToken || !publishFlag) {
// NOTE archiveService is an opt-in feature and no warning is needed
return BbPromise.resolve();
}
const clientWithAuth = createApolloClient(config.GRAPHQL_ENDPOINT_URL, authToken);
return this.archiveServiceRequest(this.serverless.service.service, clientWithAuth)
.then(response => {
this.serverless.cli.log('Successfully archived your service on the Serverless Platform');
return response.data;
})
.catch(err => {
this.serverless.cli.log('Failed to archived your service on the Serverless Platform');
throw new this.serverless.classes.Error(err.message);
});
}
publishServiceRequest(service, client) {
return client
.mutate({
mutation: gql`
mutation publishService($service: ServicePublishInputType!) {
publishService(service: $service) {
name
}
}
`,
variables: { service },
})
.then(response => response.data);
}
getAuthToken() {
return getAuthToken();
}
publishService() {
const authToken = this.getAuthToken();
const publishFlag = selectServicePublish(this.serverless.service);
if (!authToken || !publishFlag) {
// NOTE publishService is an opt-in feature and no warning is needed
return BbPromise.resolve();
}
this.serverless.cli.log('Publish service to Serverless Platform...');
const clientWithAuth = createApolloClient(config.GRAPHQL_ENDPOINT_URL, authToken);
const region = this.provider.getRegion();
return this.provider.getAccountInfo().then(res =>
fetchEndpoint(this.provider).then(endpoint => {
const funcs = this.serverless.service.getAllFunctions().map(key => {
const arnName = functionInfoUtils.aws.getArnName(key, this.serverless);
let funcAttributes = {
name: key,
runtime: functionInfoUtils.aws.getRuntime(key, this.serverless),
memory: functionInfoUtils.aws.getMemorySize(key, this.serverless),
timeout: functionInfoUtils.aws.getTimeout(key, this.serverless),
provider: this.serverless.service.provider.name,
originId: `arn:${res.partition}:lambda:${region}:${res.accountId}:function:${arnName}`,
endpoints: functionInfoUtils.aws.getEndpoints(key, this.serverless, endpoint),
};
if (this.serverless.service.functions[key].readme) {
funcAttributes = addReadme(
funcAttributes,
this.serverless.service.functions[key].readme
);
}
if (this.serverless.service.functions[key].description) {
funcAttributes.description = this.serverless.service.functions[key].description;
}
return funcAttributes;
return getAccessKey(this.config.tenant).then(accessKey => {
if (!accessKey || !this.config.app || !this.config.tenant) {
return BbPromise.resolve();
}
const data = {
name: this.serverless.service.service,
tenant: this.config.tenant,
app: this.config.app,
accessKey,
};
return platform.archiveService(data)
.then(() => {
this.serverless.cli.log('Successfully archived your service on the Serverless Platform');
process.exit(0);
})
.catch(err => {
this.serverless.cli.log('Failed to archived your service on the Serverless Platform');
throw new this.serverless.classes.Error(err.message);
});
const serviceData = {
name: this.serverless.service.service,
stage: this.serverless.processedInput.options.stage,
functions: funcs,
};
if (this.serverless.service.serviceObject.description) {
serviceData.description = this.serverless.service.serviceObject.description;
}
if (this.serverless.service.serviceObject.license) {
serviceData.license = this.serverless.service.serviceObject.license;
}
if (this.serverless.service.serviceObject.bugs) {
serviceData.bugs = this.serverless.service.serviceObject.bugs;
}
if (this.serverless.service.serviceObject.repository) {
serviceData.repository = this.serverless.service.serviceObject.repository;
}
if (this.serverless.service.serviceObject.homepage) {
serviceData.homepage = this.serverless.service.serviceObject.homepage;
}
// NOTE can be improved by making sure it captures multiple variations
// of readme file name e.g. readme.md, readme.txt, Readme.md
const readmePath = path.join(this.serverless.config.servicePath, 'README.md');
const serviceDataWithReadme = addReadme(serviceData, readmePath);
return new BbPromise((resolve, reject) => {
this.publishServiceRequest(serviceDataWithReadme, clientWithAuth)
.then(() => {
const username = jwtDecode(authToken).nickname;
const serviceName = this.serverless.service.service;
const url = `${config.PLATFORM_FRONTEND_BASE_URL}services/${username}/${serviceName}`;
console.log('Service successfully published! Your service details are available at:');
console.log(chalk.green(url));
resolve();
})
.catch(error => {
this.serverless.cli.log(
"Couldn't publish this deploy information to the Serverless Platform."
);
reject(error);
});
});
})
);
});
}
}

View File

@ -1,24 +0,0 @@
const apollo = require('apollo-client');
module.exports = (endpoint, auth0IdToken) => {
const networkInterface = apollo.createNetworkInterface({ uri: endpoint });
if (auth0IdToken) {
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
// eslint-disable-next-line no-param-reassign
req.options.headers = {};
}
const token = auth0IdToken;
// eslint-disable-next-line no-param-reassign
req.options.headers.authorization = token ? `Bearer ${token}` : null;
next();
},
}]);
}
return new apollo.ApolloClient({
networkInterface,
});
};

39
lib/utils/getAccessKey.js Normal file
View File

@ -0,0 +1,39 @@
'use strict';
const configUtils = require('./config');
const { createAccessKey } = require('@serverless/platform-sdk');
const BbPromise = require('bluebird');
function getAccessKey(tenant) {
if (process.env.SERVERLESS_ACCESS_KEY) {
return BbPromise.resolve(process.env.SERVERLESS_ACCESS_KEY);
}
if (!tenant) {
return BbPromise.resolve(null);
}
const userConfig = configUtils.getConfig();
const currentId = userConfig.userId;
const globalConfig = configUtils.getGlobalConfig();
if (globalConfig.users && globalConfig.users[currentId] &&
globalConfig.users[currentId].dashboard) {
if (globalConfig.users[currentId].dashboard.accessKey) {
return BbPromise.resolve(globalConfig.users[currentId].dashboard.accessKey);
} else if (globalConfig.users[currentId].dashboard.idToken) {
const data = {
tenant,
username: globalConfig.users[currentId].username,
idToken: globalConfig.users[currentId].dashboard.idToken,
title: 'Framework',
};
return createAccessKey(data).then(res => res.json()).then(res => {
globalConfig.users[currentId].dashboard.accessKey = res.secretAccessKey;
configUtils.set(globalConfig);
return res.secretAccessKey;
});
}
}
return BbPromise.resolve(null);
}
module.exports = getAccessKey;

View File

@ -1,31 +0,0 @@
'use strict';
/* eslint-disable no-console */
const opn = require('opn');
const chalk = require('chalk');
const isDockerContainer = require('is-docker');
function displayManualOpenMessage(url, err) {
// https://github.com/sindresorhus/log-symbols
console.log('---------------------------');
const errMsg = err ? `\nError: ${err.message}` : '';
const msg = `Unable to open browser automatically${errMsg}`;
console.log(`🙈 ${chalk.red(msg)}`);
console.log(chalk.green('Please open your browser & open the URL below to login:'));
console.log(chalk.yellow(url));
console.log('---------------------------');
return false;
}
module.exports = function openBrowser(url) {
let browser = process.env.BROWSER;
if (browser === 'none' || isDockerContainer()) {
return displayManualOpenMessage(url);
}
if (process.platform === 'darwin' && browser === 'open') {
browser = undefined;
}
const options = { app: browser };
return opn(url, options).catch(err => displayManualOpenMessage(url, err));
};

View File

@ -1,37 +0,0 @@
'use strict';
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const expect = require('chai').expect;
describe('#openBrowser', () => {
let openBrowser;
let opnStub;
let isDockerStub;
beforeEach(() => {
opnStub = sinon.stub().resolves({});
opnStub = sinon.stub().resolves({});
isDockerStub = sinon.stub().returns(false);
openBrowser = proxyquire('./openBrowser', {
opn: opnStub,
'is-docker': isDockerStub,
});
});
it('should open the browser with the provided url', () => {
openBrowser('http://www.example.com');
expect(opnStub.getCall(0).args[0]).to.equal('http://www.example.com');
});
it('should open the browser with the provided url', () => {
isDockerStub = sinon.stub().returns(true);
openBrowser = proxyquire('./openBrowser', {
opn: opnStub,
'is-docker': isDockerStub,
});
openBrowser('http://www.example.com');
expect(opnStub.notCalled).to.equal(true);
});
});

View File

@ -1,7 +0,0 @@
'use strict';
const _ = require('lodash');
const selectServicePublish = (service) => _.get(service, 'serviceObject.publish', true);
module.exports = selectServicePublish;

View File

@ -1,18 +0,0 @@
'use strict';
const expect = require('chai').expect;
const selectServicePublish = require('./selectServicePublish');
describe('#selectServicePublish()', () => {
it('should return the publish value of the service object', () => {
const service = {
serviceObject: {
publish: true,
},
};
const result = selectServicePublish(service);
expect(result).to.equal(true);
});
});

View File

@ -92,7 +92,7 @@
},
"dependencies": {
"@serverless/fdk": "^0.5.1",
"apollo-client": "^1.9.2",
"@serverless/platform-sdk": "file:../platform-sdk",
"archiver": "^1.1.0",
"async": "^1.5.2",
"aws-sdk": "^2.228.0",
@ -106,8 +106,6 @@
"get-stdin": "^5.0.1",
"globby": "^6.1.0",
"graceful-fs": "^4.1.11",
"graphql": "^0.10.1",
"graphql-tag": "^2.4.0",
"https-proxy-agent": "^2.2.1",
"is-docker": "^1.1.0",
"js-yaml": "^3.6.1",
@ -118,9 +116,7 @@
"minimist": "^1.2.0",
"moment": "^2.13.0",
"node-fetch": "^1.6.0",
"node-forge": "^0.7.1",
"object-hash": "^1.2.0",
"opn": "^5.0.0",
"promise-queue": "^2.2.3",
"raven": "^1.2.1",
"rc": "^1.1.6",

6
test/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# package directories
node_modules
jspm_packages
# Serverless directories
.serverless

16
test/handler.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
module.exports.hello = (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}),
};
callback(null, response);
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};

18
test/serverless.yml Normal file
View File

@ -0,0 +1,18 @@
service: eslam-serverless-dashboard-demo
tenant: eahefnawy
app: framework
provider:
name: aws
runtime: nodejs6.10
functions:
hello:
handler: handler.hello
events:
- user.created
- http:
path: test-again
method: post
cors: true