Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Gateway Owntracks messages #574

Merged
merged 4 commits into from
Oct 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion front/src/actions/gatewayLinkUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ function createActions(store) {
});
} catch (e) {
console.log(e);
const error = get(e, 'response.error');
const errorMessage = get(e, 'response.error_message');
const errorMessageOtherFormat = get(e, 'response.message');
if (errorMessage === 'NO_INSTANCE_FOUND' || errorMessageOtherFormat === 'NO_INSTANCE_DETECTED') {
if (error === 'LINKED_USER_NOT_FOUND') {
await state.session.gatewayClient.updateUserIdInGladys(null);
window.location = '/link-gateway-user';
} else if (errorMessage === 'NO_INSTANCE_FOUND' || errorMessageOtherFormat === 'NO_INSTANCE_DETECTED') {
store.setState({
usersGetStatus: RequestStatus.GatewayNoInstanceFound
});
Expand Down
6 changes: 6 additions & 0 deletions front/src/actions/login/loginGateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ function createActions(store) {
route('/link-gateway-user');
}
} catch (e) {
console.log(e);
const error = get(e, 'response.error');
const errorMessage = get(e, 'response.error_message');
// if user was previously linked to another instance, we reset the user id
if (error === 'LINKED_USER_NOT_FOUND') {
await state.session.gatewayClient.updateUserIdInGladys(null);
Expand All @@ -94,6 +96,10 @@ function createActions(store) {
store.setState({
gatewayLoginStatus: RequestStatus.UserNotAcceptedLocally
});
} else if (errorMessage === 'NO_INSTANCE_FOUND') {
store.setState({
gatewayLoginStatus: RequestStatus.GatewayNoInstanceFound
});
} else {
store.setState({
gatewayLoginStatus: RequestStatus.Error
Expand Down
6 changes: 6 additions & 0 deletions front/src/components/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import SettingsGateway from '../routes/settings/settings-gateway';
import SettingsBackup from '../routes/settings/settings-backup';
import SettingsBilling from '../routes/settings/settings-billing';
import SettingsGatewayUsers from '../routes/settings/settings-gateway-users';
import SettingsGatewayOpenApi from '../routes/settings/settings-gateway-open-api';

// Integrations
import TelegramPage from '../routes/integration/all/telegram';
Expand Down Expand Up @@ -118,6 +119,11 @@ const AppRouter = connect(
) : (
<Error type="404" default />
)}
{config.gatewayMode ? (
<SettingsGatewayOpenApi path="/dashboard/settings/gateway-open-api" />
) : (
<Error type="404" default />
)}

{!config.gatewayMode ? <SignupWelcomePage path="/signup" /> : <Error type="404" default />}
<SignupCreateAccountLocal path="/signup/create-account-local" />
Expand Down
8 changes: 8 additions & 0 deletions front/src/components/gateway/GatewayLoginForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ const GatewayLoginForm = ({ children, ...props }) => (
<Text id="gatewayLogin.userNotAcceptedLocally" />
</div>
)}
{props.gatewayLoginStatus === RequestStatus.GatewayNoInstanceFound && (
<div class="alert alert-danger" role="alert">
<Text id="gatewayLogin.gatewayNoInstanceFound" />{' '}
<a href="/dashboard/settings/billing">
<Text id="gatewayLogin.gatewayAccessBilling" />
</a>
</div>
)}
{props.gatewayLoginStatus === RequestStatus.NetworkError && (
<div class="alert alert-danger" role="alert">
<Text id="gatewayLogin.networkError" />
Expand Down
16 changes: 15 additions & 1 deletion front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,8 @@
"gatewayTab": "Gladys Plus",
"backupTab": "Backups",
"billingTab": "Billing",
"gatewayUsersTab": "Users"
"gatewayUsersTab": "Users",
"gatewayOpenApiTab": "Open API"
},
"housesSettings": {
"searchPlaceholder": "Search houses",
Expand Down Expand Up @@ -576,6 +577,8 @@
"invalidEmail": "Invalid email",
"networkError": "Network Error: Are you sure your Gladys instance is connected to the internet?",
"userNotAcceptedLocally": "Your user is not accepted locally, please go back to your Gladys instance and allow access to this user in settings.",
"gatewayNoInstanceFound": "No Instance found. You need to connect first your instance to Gladys.",
"gatewayAccessBilling": "To access billing, click here.",
"unknownError": "An unknown error occurred. Please contact us for more informations.",
"twoFactorCodeLabel": "Two Factor code",
"twoFactorCodePlaceholder": "6 digits code",
Expand Down Expand Up @@ -762,5 +765,16 @@
"gatewayResetPassword": {
"pageTitle": "Gladys Assistant",
"formTitle": "Gladys Plus - Reset Password"
},
"gatewayOpenApi": {
"title": "Open API",
"description": "The Open API allows you to send informations to Gladys outside of your network. For now, it allows you to send Owntracks data only.",
"moreInformations": "More informations.",
"warningKeyDisappear": "Warning: You'll see the key only once. Copy paste your key and save it somewhere.",
"keyName": "Name",
"keyLastUsed": "Last used",
"revoke": "Revoke",
"generateButton": "Generate",
"registered": "Registered"
}
}
13 changes: 13 additions & 0 deletions front/src/routes/settings/SettingsLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ const DashboardSettings = ({ children, ...props }) => (
</Link>
)}

{config.gatewayMode && (
<Link
href="/dashboard/settings/gateway-open-api"
activeClassName="active"
class="list-group-item list-group-item-action d-flex align-items-center"
>
<span class="icon mr-3">
<i class="fe fe-globe" />
</span>
<Text id="settings.gatewayOpenApiTab" />
</Link>
)}

{config.gatewayMode && (
<Link
href="/dashboard/settings/billing"
Expand Down
71 changes: 71 additions & 0 deletions front/src/routes/settings/settings-gateway-open-api/OpenApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Text, Localizer } from 'preact-i18n';

import OpenApiKey from './OpenApiKey';

const OpenApi = ({ children, ...props }) => (
<div class="card">
<div class="card-header">
<h3 class="card-title">
<Text id="gatewayOpenApi.title" />
</h3>
</div>
<div class="card-body">
<p>
<Text id="gatewayOpenApi.description" />{' '}
<a href="https://documentation.gladysassistant.com/en/configuration#gateway-open-api">
<Text id="gatewayOpenApi.moreInformations" />
</a>
</p>
<p>
<Text id="gatewayOpenApi.warningKeyDisappear" />
</p>
</div>
<div>
<div class="table-responsive">
<table class="table table-hover table-outline table-vcenter text-nowrap card-table">
<thead>
<tr>
<th>
<Text id="gatewayOpenApi.keyName" />
</th>
<th>
<Text id="gatewayOpenApi.keyLastUsed" />
</th>
<th class="w-1">
<Text id="gatewayOpenApi.revoke" />
</th>
</tr>
</thead>
<tbody>
{props.apiKeys &&
props.apiKeys.map((apiKey, index) => (
<OpenApiKey apiKey={apiKey} revokeOpenApiKey={props.revokeOpenApiKey} index={index} />
))}

<tr>
<td>
<Localizer>
<input
type="text"
class={'form-control ' + (props.missingNewOpenApiName ? 'is-invalid' : '')}
value={props.newApiKeyName}
onChange={props.updateNewApiKeyName}
placeholder={<Text id="gatewayOpenApi.keyName" />}
/>
</Localizer>
</td>
<td>
<button class="btn btn-primary" onClick={props.createApiKey}>
<Text id="gatewayOpenApi.generateButton" />
</button>
</td>
<td />
</tr>
</tbody>
</table>
</div>
</div>
</div>
);

export default OpenApi;
43 changes: 43 additions & 0 deletions front/src/routes/settings/settings-gateway-open-api/OpenApiKey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Text } from 'preact-i18n';

const dateDisplayOptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };

const OpenApiKey = ({ children, ...props }) => {
let revokeOpenApiKey = e => {
e.preventDefault();
props.revokeOpenApiKey(props.apiKey.id, props.index);
};

let createdAt = new Date(props.apiKey.created_at).toLocaleDateString('en-US', dateDisplayOptions);
let lastUsed =
props.apiKey.last_used === null
? 'never'
: new Date(props.apiKey.last_used).toLocaleDateString('en-US', dateDisplayOptions);

return (
<tr>
<td>
<div style="max-width: 400px; overflow: hidden">{props.apiKey.name}</div>
{props.apiKey.api_key && (
<div class="small">
<Text id="gatewayOpenApi.keyName" />: {props.apiKey.api_key}
</div>
)}
<div class="small text-muted">
<Text id="gatewayOpenApi.registered" />: {createdAt}
</div>
</td>
<td>
<div class="small text-muted">
<Text id="gatewayOpenApi.keyLastUsed" />
</div>
<div>{lastUsed}</div>
</td>
<td>
<i style={{ cursor: 'pointer' }} onClick={revokeOpenApiKey} class="fe fe-trash-2" />
</td>
</tr>
);
};

export default OpenApiKey;
64 changes: 64 additions & 0 deletions front/src/routes/settings/settings-gateway-open-api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import SettingsLayout from '../SettingsLayout';
import update from 'immutability-helper';
import OpenApi from './OpenApi';
import linkState from 'linkstate';

@connect(
'session',
{}
)
class SettingsGatewayOpenApi extends Component {
state = {
apiKeys: [],
newApiKeyName: ''
};

getApiKeys = async () => {
const apiKeys = await this.props.session.gatewayClient.getApiKeys();
this.setState({ apiKeys });
};

createApiKey = async () => {
if (!this.state.newApiKeyName || this.state.newApiKeyName.length === 0) {
return this.setState({ missingNewOpenApiName: true });
}

const apiKey = await this.props.session.gatewayClient.createApiKey(this.state.newApiKeyName);
const newState = update(this.state, {
apiKeys: { $push: [apiKey] },
newApiKey: { $set: apiKey },
newApiKeyName: { $set: '' },
missingNewOpenApiName: { $set: false }
});
this.setState(newState);
};

revokeOpenApiKey = async (id, index) => {
await this.props.session.gatewayClient.revokeApiKey(id);
const newState = update(this.state, {
apiKeys: { $splice: [[index, 1]] }
});
this.setState(newState);
};

componentDidMount() {
this.getApiKeys();
}

render({}, props) {
return (
<SettingsLayout>
<OpenApi
{...props}
createApiKey={this.createApiKey}
revokeOpenApiKey={this.revokeOpenApiKey}
updateNewApiKeyName={linkState(this, 'newApiKeyName')}
/>
</SettingsLayout>
);
}
}

export default SettingsGatewayOpenApi;
52 changes: 29 additions & 23 deletions server/lib/gateway/gateway.handleNewMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,31 @@ const { EVENTS } = require('../../utils/constants');
* });
*/
async function handleNewMessage(data, rawMessage, cb) {
// first, we verify that the user has the right to control the instance
const usersKeys = JSON.parse(await this.variable.getValue('GLADYS_GATEWAY_USERS_KEYS'));
const rsaPublicKey = await this.gladysGatewayClient.generateFingerprint(rawMessage.rsaPublicKeyRaw);
const ecdsaPublicKey = await this.gladysGatewayClient.generateFingerprint(rawMessage.ecdsaPublicKeyRaw);
if (data.type === 'gladys-api-call') {
// first, we verify that the user has the right to control the instance
const usersKeys = JSON.parse(await this.variable.getValue('GLADYS_GATEWAY_USERS_KEYS'));
const rsaPublicKey = await this.gladysGatewayClient.generateFingerprint(rawMessage.rsaPublicKeyRaw);
const ecdsaPublicKey = await this.gladysGatewayClient.generateFingerprint(rawMessage.ecdsaPublicKeyRaw);

const found = usersKeys.find(
(user) => user.rsa_public_key === rsaPublicKey && user.ecdsa_public_key === ecdsaPublicKey,
);
const found = usersKeys.find(
(user) => user.rsa_public_key === rsaPublicKey && user.ecdsa_public_key === ecdsaPublicKey,
);

if ((!found || !found.accepted) && get(data, 'options.url') !== '/api/v1/user') {
cb({
status: 403,
error: 'USER_NOT_ACCEPTED_LOCALLY',
message: 'User not allowed to control this Gladys instance',
});
return;
}
if (!rawMessage.local_user_id && get(data, 'options.url') !== '/api/v1/user') {
cb({
status: 400,
error: 'GATEWAY_USER_NOT_LINKED',
});
return;
}
if (data.type === 'gladys-api-call') {
if ((!found || !found.accepted) && get(data, 'options.url') !== '/api/v1/user') {
cb({
status: 403,
error: 'USER_NOT_ACCEPTED_LOCALLY',
message: 'User not allowed to control this Gladys instance',
});
return;
}
if (!rawMessage.local_user_id && get(data, 'options.url') !== '/api/v1/user') {
cb({
status: 400,
error: 'GATEWAY_USER_NOT_LINKED',
});
return;
}
try {
const user = rawMessage.local_user_id ? await this.user.getById(rawMessage.local_user_id) : null;
this.event.emit(
Expand All @@ -62,6 +62,12 @@ async function handleNewMessage(data, rawMessage, cb) {
}
}
}

// if the message is an open API message
if (data.type === 'gladys-open-api' && data.action === 'create-owntracks-location') {
this.event.emit(EVENTS.GATEWAY.NEW_MESSAGE_OWNTRACKS_LOCATION, data.data);
cb({ status: 200 });
}
}

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion server/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ function Gladys(params = {}) {
const house = new House(event);
const room = new Room(brain);
const service = new Service(services, stateManager);
const location = new Location();
const message = new MessageHandler(event, brain, service);
const session = new Session(params.jwtSecret, cache);
const user = new User(session, stateManager, variable);
const location = new Location(user, event);
const device = new Device(event, message, stateManager, service, room, variable);
const scene = new Scene(stateManager, event, device);
const scheduler = new Scheduler(event);
Expand Down
Loading