diff --git a/server/Database.ts b/server/Database.ts index 25074b7..f59edea 100644 --- a/server/Database.ts +++ b/server/Database.ts @@ -39,7 +39,9 @@ export class Database { async createAll() { await this.conn.synchronize(); - await this.insertData(); + if (process.env.INSERT_TEST_DATA) { + await this.insertData(); + } } async insertData() { @@ -62,10 +64,9 @@ export class Database { const name = "test" + i; let device = await this.sprinklersDevices.findByName(name); if (!device) { - device = await this.sprinklersDevices.create({ - name, - }); + device = await this.sprinklersDevices.create(); } + Object.assign(device, { name, deviceId: "grinklers" + (i === 1 ? "" : i) }); await this.sprinklersDevices.save(device); for (let j = 0; j < 5; j++) { const userIdx = (i + j * 10) % NUM; diff --git a/server/entities/SprinklersDevice.ts b/server/entities/SprinklersDevice.ts index 176c1a6..48a6594 100644 --- a/server/entities/SprinklersDevice.ts +++ b/server/entities/SprinklersDevice.ts @@ -7,7 +7,7 @@ export class SprinklersDevice { @PrimaryGeneratedColumn() id!: number; - @Column({ nullable: true, type: "uuid" }) + @Column({ unique: true, nullable: true, type: "varchar" }) deviceId: string | null = null; @Column() diff --git a/server/express/api.ts b/server/express/api.ts deleted file mode 100644 index e2f96d4..0000000 --- a/server/express/api.ts +++ /dev/null @@ -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; -} diff --git a/server/express/api/devices.ts b/server/express/api/devices.ts new file mode 100644 index 0000000..9d038e5 --- /dev/null +++ b/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; +} diff --git a/server/express/api/index.ts b/server/express/api/index.ts new file mode 100644 index 0000000..de9f23d --- /dev/null +++ b/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; +} diff --git a/server/express/api/users.ts b/server/express/api/users.ts new file mode 100644 index 0000000..034d9e9 --- /dev/null +++ b/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 { + 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; +} diff --git a/server/express/authentication.ts b/server/express/authentication.ts index 5d606c4..4860971 100644 --- a/server/express/authentication.ts +++ b/server/express/authentication.ts @@ -142,7 +142,7 @@ export function authentication(state: ServerState) { return user; } - router.post("/token/grant", async (req, res) => { + router.post("/grant", async (req, res) => { const body: TokenGrantRequest = req.body; let user: User; if (body.grant_type === "password") { @@ -161,12 +161,12 @@ export function authentication(state: ServerState) { 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); res.json({ token }); }); - router.post("/token/verify", verifyAuthorization(), async (req, res) => { + router.post("/verify", verifyAuthorization(), async (req, res) => { res.json({ ok: true, token: req.token, diff --git a/server/repositories/SprinklersDeviceRepository.ts b/server/repositories/SprinklersDeviceRepository.ts index ac1e0da..f43301d 100644 --- a/server/repositories/SprinklersDeviceRepository.ts +++ b/server/repositories/SprinklersDeviceRepository.ts @@ -1,10 +1,33 @@ import { EntityRepository, Repository } from "typeorm"; -import { SprinklersDevice } from "../entities"; +import { SprinklersDevice, User } from "../entities"; @EntityRepository(SprinklersDevice) export class SprinklersDeviceRepository extends Repository { findByName(name: string) { return this.findOne({ name }); } + + async userHasAccess(userId: number, deviceId: number): Promise { + 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 { + 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]; + } } diff --git a/server/repositories/UserRepository.ts b/server/repositories/UserRepository.ts index 0f625a5..61873e2 100644 --- a/server/repositories/UserRepository.ts +++ b/server/repositories/UserRepository.ts @@ -2,9 +2,27 @@ import { EntityRepository, Repository } from "typeorm"; import { User } from "../entities"; +export interface FindUserOptions { + devices: boolean; +} + +function applyDefaultOptions(options?: Partial): FindUserOptions { + return { devices: false, ...options }; +} + @EntityRepository(User) export class UserRepository extends Repository { - findByUsername(username: string) { - return this.findOne({ username }, { relations: ["devices"] }); + findAll(options?: Partial) { + const opts = applyDefaultOptions(options); + const relations = [ opts.devices && "devices" ] + .filter(Boolean) as string[]; + return super.find({ relations }); + } + + findByUsername(username: string, options?: Partial) { + const opts = applyDefaultOptions(options); + const relations = [ opts.devices && "devices" ] + .filter(Boolean) as string[]; + return this.findOne({ username }, { relations }); } } diff --git a/server/sprinklersRpc/websocketServer.ts b/server/sprinklersRpc/websocketServer.ts index 9df2ac8..df1773d 100644 --- a/server/sprinklersRpc/websocketServer.ts +++ b/server/sprinklersRpc/websocketServer.ts @@ -82,8 +82,11 @@ export class WebSocketClient { }, deviceSubscribe: async (data: ws.IDeviceSubscribeRequest) => { this.checkAuthorization(); + const userId = this.userId!; 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 { result: "error", error: { code: ErrorCode.NoPermission, diff --git a/server/tsconfig.json b/server/tsconfig.json index b7afe72..2e5e43b 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -21,8 +21,8 @@ "@common/*": [ "./common/*" ], - "@client/*": [ - "./client/*" + "@server/*": [ + "./server/*" ] } },