Browse Source

Nice refactoring and verification of api endpoints

update-deps
Alex Mikhalev 7 years ago
parent
commit
ab0756d01e
  1. 9
      server/Database.ts
  2. 2
      server/entities/SprinklersDevice.ts
  3. 52
      server/express/api.ts
  4. 35
      server/express/api/devices.ts
  5. 22
      server/express/api/index.ts
  6. 48
      server/express/api/users.ts
  7. 6
      server/express/authentication.ts
  8. 25
      server/repositories/SprinklersDeviceRepository.ts
  9. 22
      server/repositories/UserRepository.ts
  10. 5
      server/sprinklersRpc/websocketServer.ts
  11. 4
      server/tsconfig.json

9
server/Database.ts

@ -39,7 +39,9 @@ export class Database {
async createAll() { async createAll() {
await this.conn.synchronize(); await this.conn.synchronize();
await this.insertData(); if (process.env.INSERT_TEST_DATA) {
await this.insertData();
}
} }
async insertData() { async insertData() {
@ -62,10 +64,9 @@ export class Database {
const name = "test" + i; const name = "test" + i;
let device = await this.sprinklersDevices.findByName(name); let device = await this.sprinklersDevices.findByName(name);
if (!device) { if (!device) {
device = await this.sprinklersDevices.create({ device = await this.sprinklersDevices.create();
name,
});
} }
Object.assign(device, { name, deviceId: "grinklers" + (i === 1 ? "" : i) });
await this.sprinklersDevices.save(device); await this.sprinklersDevices.save(device);
for (let j = 0; j < 5; j++) { for (let j = 0; j < 5; j++) {
const userIdx = (i + j * 10) % NUM; const userIdx = (i + j * 10) % NUM;

2
server/entities/SprinklersDevice.ts

@ -7,7 +7,7 @@ export class SprinklersDevice {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id!: number; id!: number;
@Column({ nullable: true, type: "uuid" }) @Column({ unique: true, nullable: true, type: "varchar" })
deviceId: string | null = null; deviceId: string | null = null;
@Column() @Column()

52
server/express/api.ts

@ -1,52 +0,0 @@
import PromiseRouter from "express-promise-router";
import { serialize} from "serializr";
import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
import * as schema from "@common/sprinklersRpc/schema";
import { ServerState } from "../state";
import { authentication, verifyAuthorization } from "./authentication";
export default function createApi(state: ServerState) {
const router = PromiseRouter();
router.get("/devices/:deviceId", verifyAuthorization(), (req, res) => {
// TODO: authorize device
const device = state.mqttClient.getDevice(req.params.deviceId);
const j = serialize(schema.sprinklersDevice, device);
res.send(j);
});
// router.post("/devices/register", verifyAuthorization({
// type: "device_reg",
// }), (req, res) => {
// res.json({ data: "device registered" });
// });
router.get("/users", verifyAuthorization(), (req, res) => {
state.database.users.find()
.then((users) => {
res.json({
data: users,
});
});
});
router.get("/api/users/:username", (req, res, next) => {
const { username } = req.params;
state.database.users.findByUsername(username)
.then((user) => {
if (!user) {
throw new ApiError(`user ${username} does not exist`, ErrorCode.NotFound);
}
res.json({
data: user,
});
})
.catch(next);
});
router.use("/", authentication(state));
return router;
}

35
server/express/api/devices.ts

@ -0,0 +1,35 @@
import PromiseRouter from "express-promise-router";
import { serialize} from "serializr";
import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
import * as schema from "@common/sprinklersRpc/schema";
import { AccessOrRefreshToken } from "@common/TokenClaims";
import { verifyAuthorization } from "@server/express/authentication";
import { ServerState } from "@server/state";
export function devices(state: ServerState) {
const router = PromiseRouter();
router.get("/:deviceId", verifyAuthorization(), async (req, res) => {
const token = req.token! as AccessOrRefreshToken;
const userId = token.aud;
const deviceId = req.params.deviceId;
const userDevice = await state.database.sprinklersDevices
.findUserDevice(userId, deviceId);
if (!userDevice) {
throw new ApiError("User does not have access to the specified device", ErrorCode.NoPermission);
}
const device = state.mqttClient.getDevice(req.params.deviceId);
const j = serialize(schema.sprinklersDevice, device);
res.send(j);
});
router.post("/register", verifyAuthorization({
type: "device_reg",
}), async (req, res) => {
});
return router;
}

22
server/express/api/index.ts

@ -0,0 +1,22 @@
import PromiseRouter from "express-promise-router";
import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
import { authentication } from "@server/express/authentication";
import { ServerState } from "@server/state";
import { devices } from "./devices";
import { users } from "./users";
export default function createApi(state: ServerState) {
const router = PromiseRouter();
router.use("/devices", devices(state));
router.use("/users", users(state));
router.use("/token", authentication(state));
router.use("*", (req, res) => {
throw new ApiError("API endpoint not found", ErrorCode.NotFound);
});
return router;
}

48
server/express/api/users.ts

@ -0,0 +1,48 @@
import PromiseRouter from "express-promise-router";
import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
import { User } from "@server/entities";
import { verifyAuthorization } from "@server/express/authentication";
import { ServerState } from "@server/state";
export function users(state: ServerState) {
const router = PromiseRouter();
router.use(verifyAuthorization());
async function getUser(params: { username: string }): Promise<User> {
const { username } = params;
const user = await state.database.users
.findByUsername(username, { devices: true });
if (!user) {
throw new ApiError(`user ${username} does not exist`, ErrorCode.NotFound);
}
return user;
}
router.get("/", (req, res) => {
state.database.users.findAll()
.then((users) => {
res.json({
data: users,
});
});
});
router.get("/:username", async (req, res) => {
const user = await getUser(req.params);
res.json({
data: user,
});
});
router.get("/:username/devices", async (req, res) => {
const user = await getUser(req.params);
res.json({
data: user.devices,
});
});
return router;
}

6
server/express/authentication.ts

@ -142,7 +142,7 @@ export function authentication(state: ServerState) {
return user; return user;
} }
router.post("/token/grant", async (req, res) => { router.post("/grant", async (req, res) => {
const body: TokenGrantRequest = req.body; const body: TokenGrantRequest = req.body;
let user: User; let user: User;
if (body.grant_type === "password") { if (body.grant_type === "password") {
@ -161,12 +161,12 @@ export function authentication(state: ServerState) {
res.json(response); res.json(response);
}); });
router.post("/token/grant_device_reg", verifyAuthorization(), async (req, res) => { router.post("/grant_device_reg", verifyAuthorization(), async (req, res) => {
const token = await generateDeviceRegistrationToken(JWT_SECRET); const token = await generateDeviceRegistrationToken(JWT_SECRET);
res.json({ token }); res.json({ token });
}); });
router.post("/token/verify", verifyAuthorization(), async (req, res) => { router.post("/verify", verifyAuthorization(), async (req, res) => {
res.json({ res.json({
ok: true, ok: true,
token: req.token, token: req.token,

25
server/repositories/SprinklersDeviceRepository.ts

@ -1,10 +1,33 @@
import { EntityRepository, Repository } from "typeorm"; import { EntityRepository, Repository } from "typeorm";
import { SprinklersDevice } from "../entities"; import { SprinklersDevice, User } from "../entities";
@EntityRepository(SprinklersDevice) @EntityRepository(SprinklersDevice)
export class SprinklersDeviceRepository extends Repository<SprinklersDevice> { export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
findByName(name: string) { findByName(name: string) {
return this.findOne({ name }); return this.findOne({ name });
} }
async userHasAccess(userId: number, deviceId: number): Promise<boolean> {
const count = await this.manager
.createQueryBuilder(User, "user")
.innerJoinAndSelect("user.devices", "sprinklers_device",
"user.id = :userId AND sprinklers_device.id = :deviceId",
{ userId, deviceId })
.getCount();
return count > 0;
}
async findUserDevice(userId: number, deviceId: number): Promise<SprinklersDevice | null> {
const user = await this.manager
.createQueryBuilder(User, "user")
.innerJoinAndSelect("user.devices", "sprinklers_device",
"user.id = :userId AND sprinklers_device.id = :deviceId",
{ userId, deviceId })
.getOne();
if (!user) {
return null;
}
return user.devices![0];
}
} }

22
server/repositories/UserRepository.ts

@ -2,9 +2,27 @@ import { EntityRepository, Repository } from "typeorm";
import { User } from "../entities"; import { User } from "../entities";
export interface FindUserOptions {
devices: boolean;
}
function applyDefaultOptions(options?: Partial<FindUserOptions>): FindUserOptions {
return { devices: false, ...options };
}
@EntityRepository(User) @EntityRepository(User)
export class UserRepository extends Repository<User> { export class UserRepository extends Repository<User> {
findByUsername(username: string) { findAll(options?: Partial<FindUserOptions>) {
return this.findOne({ username }, { relations: ["devices"] }); const opts = applyDefaultOptions(options);
const relations = [ opts.devices && "devices" ]
.filter(Boolean) as string[];
return super.find({ relations });
}
findByUsername(username: string, options?: Partial<FindUserOptions>) {
const opts = applyDefaultOptions(options);
const relations = [ opts.devices && "devices" ]
.filter(Boolean) as string[];
return this.findOne({ username }, { relations });
} }
} }

5
server/sprinklersRpc/websocketServer.ts

@ -82,8 +82,11 @@ export class WebSocketClient {
}, },
deviceSubscribe: async (data: ws.IDeviceSubscribeRequest) => { deviceSubscribe: async (data: ws.IDeviceSubscribeRequest) => {
this.checkAuthorization(); this.checkAuthorization();
const userId = this.userId!;
const deviceId = data.deviceId; const deviceId = data.deviceId;
if (deviceId !== "grinklers") { // TODO: somehow validate this device id? const userDevice = await this.state.database.sprinklersDevices
.findUserDevice(userId, deviceId as any); // TODO: should be number device id
if (userDevice !== "grinklers") {
return { return {
result: "error", error: { result: "error", error: {
code: ErrorCode.NoPermission, code: ErrorCode.NoPermission,

4
server/tsconfig.json

@ -21,8 +21,8 @@
"@common/*": [ "@common/*": [
"./common/*" "./common/*"
], ],
"@client/*": [ "@server/*": [
"./client/*" "./server/*"
] ]
} }
}, },

Loading…
Cancel
Save