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() {
|
||||
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;
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -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<SprinklersDevice> {
|
||||
findByName(name: string) {
|
||||
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";
|
||||
|
||||
export interface FindUserOptions {
|
||||
devices: boolean;
|
||||
}
|
||||
|
||||
function applyDefaultOptions(options?: Partial<FindUserOptions>): FindUserOptions {
|
||||
return { devices: false, ...options };
|
||||
}
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {
|
||||
findByUsername(username: string) {
|
||||
return this.findOne({ username }, { relations: ["devices"] });
|
||||
findAll(options?: Partial<FindUserOptions>) {
|
||||
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) => {
|
||||
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,
|
||||
|
@ -21,8 +21,8 @@
|
||||
"@common/*": [
|
||||
"./common/*"
|
||||
],
|
||||
"@client/*": [
|
||||
"./client/*"
|
||||
"@server/*": [
|
||||
"./server/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user