Nice refactoring and verification of api endpoints
This commit is contained in:
parent
4dd28098bf
commit
ab0756d01e
@ -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;
|
||||||
|
@ -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()
|
||||||
|
@ -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
Normal file
35
server/express/api/devices.ts
Normal file
@ -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
Normal file
22
server/express/api/index.ts
Normal file
@ -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
Normal file
48
server/express/api/users.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
"@common/*": [
|
"@common/*": [
|
||||||
"./common/*"
|
"./common/*"
|
||||||
],
|
],
|
||||||
"@client/*": [
|
"@server/*": [
|
||||||
"./client/*"
|
"./server/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user