Fixed authentication and checking device authorization
This commit is contained in:
parent
ab0756d01e
commit
b59fbb456b
@ -22,11 +22,11 @@ const ConnectionState = observer(({ connectionState, className }:
|
||||
connectionText = "Connected";
|
||||
iconName = "linkify";
|
||||
clazzName = "connected";
|
||||
} else if (connected === false) {
|
||||
connectionText = "Device Disconnected";
|
||||
} else if (connectionState.noPermission) {
|
||||
connectionText = "No permission for this device";
|
||||
iconName = "ban";
|
||||
} else if (connected === false) {
|
||||
connectionText = "Device Disconnected";
|
||||
} else if (connectionState.clientToServer === false) {
|
||||
connectionText = "Disconnected from server";
|
||||
} else {
|
||||
|
@ -2,6 +2,7 @@ import { action, autorun, observable, when } from "mobx";
|
||||
import { update } from "serializr";
|
||||
|
||||
import { TokenStore } from "@client/state/TokenStore";
|
||||
import { UserStore } from "@client/state/UserStore";
|
||||
import { ErrorCode } from "@common/ErrorCode";
|
||||
import * as rpc from "@common/jsonRpc";
|
||||
import logger from "@common/logger";
|
||||
@ -85,6 +86,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
||||
authenticated: boolean = false;
|
||||
|
||||
tokenStore: TokenStore;
|
||||
userStore: UserStore;
|
||||
|
||||
private nextRequestId = Math.round(Math.random() * 1000000);
|
||||
private responseCallbacks: ws.ServerResponseHandlers = {};
|
||||
@ -94,9 +96,10 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
||||
return this.connectionState.isServerConnected || false;
|
||||
}
|
||||
|
||||
constructor(tokenStore: TokenStore, webSocketUrl: string = DEFAULT_URL) {
|
||||
constructor(tokenStore: TokenStore, userStore: UserStore, webSocketUrl: string = DEFAULT_URL) {
|
||||
this.webSocketUrl = webSocketUrl;
|
||||
this.tokenStore = tokenStore;
|
||||
this.userStore = userStore;
|
||||
this.connectionState.clientToServer = false;
|
||||
this.connectionState.serverToBroker = false;
|
||||
}
|
||||
@ -138,8 +141,10 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
||||
when(() => this.connectionState.clientToServer === true
|
||||
&& this.tokenStore.accessToken.isValid, async () => {
|
||||
try {
|
||||
await this.authenticate(this.tokenStore.accessToken.token!);
|
||||
this.authenticated = true;
|
||||
const res = await this.authenticate(this.tokenStore.accessToken.token!);
|
||||
this.authenticated = res.authenticated;
|
||||
logger.info({ user: res.user }, "authenticated websocket connection");
|
||||
this.userStore.userData = res.user;
|
||||
} catch (err) {
|
||||
logger.error({ err }, "error authenticating websocket connection");
|
||||
// TODO message?
|
||||
|
@ -5,6 +5,7 @@ import { RouterStore, syncHistoryWithStore } from "mobx-react-router";
|
||||
import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient";
|
||||
import HttpApi from "@client/state/HttpApi";
|
||||
import { UiStore } from "@client/state/UiStore";
|
||||
import { UserStore } from "@client/state/UserStore";
|
||||
import ApiError from "@common/ApiError";
|
||||
import { ErrorCode } from "@common/ErrorCode";
|
||||
import log from "@common/logger";
|
||||
@ -13,9 +14,10 @@ export default class AppState {
|
||||
history: History = createBrowserHistory();
|
||||
routerStore = new RouterStore();
|
||||
uiStore = new UiStore();
|
||||
userStore = new UserStore();
|
||||
httpApi = new HttpApi();
|
||||
tokenStore = this.httpApi.tokenStore;
|
||||
sprinklersRpc = new WebSocketRpcClient(this.tokenStore);
|
||||
sprinklersRpc = new WebSocketRpcClient(this.tokenStore, this.userStore);
|
||||
|
||||
@computed get isLoggedIn() {
|
||||
return this.tokenStore.accessToken.isValid;
|
||||
|
5
client/state/UserStore.ts
Normal file
5
client/state/UserStore.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { observable } from "mobx";
|
||||
|
||||
export class UserStore {
|
||||
@observable userData: any = null;
|
||||
}
|
@ -25,7 +25,7 @@ export interface IClientRequestTypes {
|
||||
export interface IAuthenticateResponse {
|
||||
authenticated: boolean;
|
||||
message: string;
|
||||
data?: any;
|
||||
user: any;
|
||||
}
|
||||
|
||||
export interface IDeviceSubscribeResponse {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EntityRepository, Repository } from "typeorm";
|
||||
import { EntityRepository, FindOneOptions, Repository } from "typeorm";
|
||||
|
||||
import { User } from "../entities";
|
||||
|
||||
@ -6,23 +6,27 @@ export interface FindUserOptions {
|
||||
devices: boolean;
|
||||
}
|
||||
|
||||
function applyDefaultOptions(options?: Partial<FindUserOptions>): FindUserOptions {
|
||||
return { devices: false, ...options };
|
||||
function applyDefaultOptions(options?: Partial<FindUserOptions>): FindOneOptions<User> {
|
||||
const opts: FindUserOptions = { devices: false, ...options };
|
||||
const relations = [opts.devices && "devices"]
|
||||
.filter(Boolean) as string[];
|
||||
return { relations };
|
||||
}
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {
|
||||
findAll(options?: Partial<FindUserOptions>) {
|
||||
const opts = applyDefaultOptions(options);
|
||||
const relations = [ opts.devices && "devices" ]
|
||||
.filter(Boolean) as string[];
|
||||
return super.find({ relations });
|
||||
return super.find(opts);
|
||||
}
|
||||
|
||||
findById(id: number, options?: Partial<FindUserOptions>) {
|
||||
const opts = applyDefaultOptions(options);
|
||||
return super.findOne(id, opts);
|
||||
}
|
||||
|
||||
findByUsername(username: string, options?: Partial<FindUserOptions>) {
|
||||
const opts = applyDefaultOptions(options);
|
||||
const relations = [ opts.devices && "devices" ]
|
||||
.filter(Boolean) as string[];
|
||||
return this.findOne({ username }, { relations });
|
||||
return this.findOne({ username }, opts);
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,9 @@ import log from "@common/logger";
|
||||
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
|
||||
import * as schema from "@common/sprinklersRpc/schema";
|
||||
import * as ws from "@common/sprinklersRpc/websocketData";
|
||||
import { TokenClaims, verifyToken } from "../express/authentication";
|
||||
import { ServerState } from "../state";
|
||||
import { User } from "@server/entities";
|
||||
import { TokenClaims, verifyToken } from "@server/express/authentication";
|
||||
import { ServerState } from "@server/state";
|
||||
|
||||
// tslint:disable:member-ordering
|
||||
|
||||
@ -22,6 +23,7 @@ export class WebSocketClient {
|
||||
|
||||
/// This shall be the user id if the client has been authenticated, null otherwise
|
||||
userId: number | null = null;
|
||||
user: User | null = null;
|
||||
|
||||
get state() {
|
||||
return this.api.state;
|
||||
@ -52,12 +54,25 @@ export class WebSocketClient {
|
||||
}
|
||||
|
||||
private checkAuthorization() {
|
||||
if (!this.userId) {
|
||||
if (!this.userId || !this.user) {
|
||||
throw new ws.RpcError("this WebSocket session has not been authenticated",
|
||||
ErrorCode.Unauthorized);
|
||||
}
|
||||
}
|
||||
|
||||
private checkDevice(devId: string) {
|
||||
const userDevice = this.user!.devices!.find((dev) => dev.deviceId === devId);
|
||||
if (userDevice == null) {
|
||||
throw new ws.RpcError("you do not have permission to subscribe to this device",
|
||||
ErrorCode.NoPermission);
|
||||
}
|
||||
const deviceId = userDevice.deviceId;
|
||||
if (!deviceId) {
|
||||
throw new ws.RpcError("device has no associated device prefix", ErrorCode.BadRequest);
|
||||
}
|
||||
return userDevice;
|
||||
}
|
||||
|
||||
private requestHandlers: ws.ClientRequestHandlers = {
|
||||
authenticate: async (data: ws.IAuthenticateRequest) => {
|
||||
if (!data.accessToken) {
|
||||
@ -73,27 +88,19 @@ export class WebSocketClient {
|
||||
throw new ws.RpcError("not an access token", ErrorCode.BadToken);
|
||||
}
|
||||
this.userId = decoded.aud;
|
||||
this.user = await this.state.database.users.
|
||||
findById(this.userId, { devices: true }) || null;
|
||||
log.info({ userId: decoded.aud, name: decoded.name }, "authenticated websocket client");
|
||||
this.subscribeBrokerConnection();
|
||||
return {
|
||||
result: "success",
|
||||
data: { authenticated: true, message: "authenticated" },
|
||||
data: { authenticated: true, message: "authenticated", user: this.user!.toJSON() },
|
||||
};
|
||||
},
|
||||
deviceSubscribe: async (data: ws.IDeviceSubscribeRequest) => {
|
||||
this.checkAuthorization();
|
||||
const userId = this.userId!;
|
||||
const deviceId = data.deviceId;
|
||||
const userDevice = await this.state.database.sprinklersDevices
|
||||
.findUserDevice(userId, deviceId as any); // TODO: should be number device id
|
||||
if (userDevice !== "grinklers") {
|
||||
return {
|
||||
result: "error", error: {
|
||||
code: ErrorCode.NoPermission,
|
||||
message: "you do not have permission to subscribe to this device",
|
||||
},
|
||||
};
|
||||
}
|
||||
const userDevice = this.checkDevice(data.deviceId);
|
||||
const deviceId = userDevice.deviceId!;
|
||||
if (this.deviceSubscriptions.indexOf(deviceId) === -1) {
|
||||
this.deviceSubscriptions.push(deviceId);
|
||||
const device = this.state.mqttClient.getDevice(deviceId);
|
||||
@ -204,10 +211,10 @@ export class WebSocketClient {
|
||||
}
|
||||
|
||||
private async doDeviceCallRequest(requestData: ws.IDeviceCallRequest): Promise<deviceRequests.Response> {
|
||||
const { deviceId, data } = requestData;
|
||||
const userDevice = this.checkDevice(requestData.deviceId);
|
||||
const deviceId = userDevice.deviceId!;
|
||||
const device = this.state.mqttClient.getDevice(deviceId);
|
||||
// TODO: authorize the requests
|
||||
const request = schema.requests.deserializeRequest(data);
|
||||
const request = schema.requests.deserializeRequest(requestData.data);
|
||||
return device.makeRequest(request);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user