Skip to content

Commit

Permalink
Implement shareable links
Browse files Browse the repository at this point in the history
  • Loading branch information
Timo authored and Dennis960 committed Apr 2, 2024
1 parent 2978c5e commit d6587bd
Show file tree
Hide file tree
Showing 18 changed files with 297 additions and 88 deletions.
6 changes: 0 additions & 6 deletions ESPlant-Server/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,16 @@ import webPushRoutes from "./routes/web-push.js";
import superjson from "./middlewares/superjson.js";
import passport from "./middlewares/passport.js";
import authRoutes from "./routes/auth.js";
import { isAuthenticated } from "./middlewares/authenticated.js";

const router = Router();
router.use(json());
router.use(cors());
router.use(passport);

// must be public
router.use(authRoutes);

// auth is handled by route
router.use(espApiRoutes);

// restricted
router.use(isAuthenticated);

router.use(superjson); // apply superjson to all dashboard API routes
router.use(sensorRoutes);
router.use(webPushRoutes);
Expand Down
79 changes: 56 additions & 23 deletions ESPlant-Server/api/config/passport.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth2";
import { Strategy as BearerStrategy } from "passport-http-bearer";
import {
Strategy as GoogleStrategy,
VerifyFunctionWithRequest as GoogleVerifyFunctionWithRequest,
} from "passport-google-oauth2";
import {
Strategy as BearerStrategy,
VerifyFunctionWithRequest as BearerVerifyFunctionWithRequest,
} from "passport-http-bearer";
import UserRepository from "../repositories/UserRepository.js";
import SensorRepository from "../repositories/SensorRepository.js";

Expand All @@ -14,8 +20,8 @@ if (GOOGLE_CLIENT_SECRET == undefined) {
throw new Error("GOOGLE_CLIENT_SECRET must be set");
}

interface AuthenticatedUser {
kind: "user" | "sensor";
export interface AuthenticatedUser {
kind: "user" | "sensor-read" | "sensor-write";
userId?: number;
sensorId?: number;
}
Expand All @@ -26,41 +32,68 @@ declare global {
}
}

const verifyGoogle: GoogleVerifyFunctionWithRequest = async (
req,
accessToken,
refreshToken,
profile,
done
) => {
if (req.user != undefined) {
// token auth overrides google auth (session)
return done(null, req.user);
}

const user = await UserRepository.findOrCreate({
googleId: profile.id,
});

return done(null, {
kind: "user",
userId: user.id,
} satisfies AuthenticatedUser);
};

passport.use(
new GoogleStrategy(
{
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL:
process.env["GOOGLE_CALLBACK_URL"] ?? "/api/auth/google/callback",
passReqToCallback: true,
},
async function (accessToken, refreshToken, profile, done) {
const user = await UserRepository.findOrCreate({
googleId: profile.id,
});

return done(null, {
kind: "user",
userId: user.id,
} satisfies AuthenticatedUser);
}
verifyGoogle
)
);

passport.use(
new BearerStrategy(async function (token, done) {
const sensorId = await SensorRepository.getIdByToken(token);
const verifyBearer: BearerVerifyFunctionWithRequest = async function (
req,
token,
done
) {
let sensorId: number | undefined = undefined;

if (sensorId == null) {
return done(null, false);
}
sensorId = await SensorRepository.getIdByWriteToken(token);
if (sensorId != undefined) {
return done(null, {
kind: "sensor-write",
sensorId,
} satisfies AuthenticatedUser);
}

sensorId = await SensorRepository.getIdByReadToken(token);
if (sensorId != undefined) {
return done(null, {
kind: "sensor",
kind: "sensor-read",
sensorId,
} satisfies AuthenticatedUser);
})
);
}

return done(null, false);
};

passport.use(new BearerStrategy({ passReqToCallback: true }, verifyBearer));

passport.serializeUser<AuthenticatedUser>(function (user, done) {
done(null, user);
Expand Down
36 changes: 29 additions & 7 deletions ESPlant-Server/api/controllers/SensorController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default class SensorController {

return {
id,
token: sensorEntity.token,
readToken: sensorEntity.readToken,
config,
lastUpdate:
lastReading != undefined
Expand All @@ -88,6 +88,15 @@ export default class SensorController {
};
}

public async getSensorWriteToken(id: number): Promise<string | undefined> {
const sensorEntity = await SensorRepository.getById(id);
if (sensorEntity == undefined) {
return undefined;
}

return sensorEntity.writeToken
}

public async getSensorOverview(ownerId: number): Promise<SensorOverviewDTO> {
const sensorsIds = await SensorRepository.getAllForOwner(ownerId);

Expand Down Expand Up @@ -239,29 +248,42 @@ export default class SensorController {
return SensorEntity.toDTO(sensorEntity);
}

private generateToken(): string {
let token = 'blumy_';
private generateToken(prefix: string, length: number) {
let token = prefix;
const possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
for (let i = 0; i < length; i++) {
let randomIndex = crypto.randomInt(0, possibleChars.length);
token += possibleChars[randomIndex];
}
return token;
}

private generateWriteToken(): string {
return this.generateToken('blumy_', 32);
}

private generateReadToken(): string {
return this.generateToken('', 16);
}

public async create(ownerId: number, config: SensorConfigurationDTO): Promise<SensorCreatedDTO> {
const token = this.generateToken();
const writeToken = this.generateWriteToken();
const readToken = this.generateReadToken();
const sensorEntityPartial = await SensorEntity.fromDTO(0, config);
const creatingSensorEntity = {
...sensorEntityPartial,
sensorAddress: undefined,
owner: ownerId,
token,
writeToken,
readToken,
};
const sensorEntity = await SensorRepository.create(creatingSensorEntity);

return {
token,
tokens: {
read: sensorEntity.readToken,
write: sensorEntity.writeToken,
},
id: sensorEntity.sensorAddress,
config: SensorEntity.toDTO(sensorEntity),
};
Expand Down
5 changes: 3 additions & 2 deletions ESPlant-Server/api/entities/SensorEntity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sharp from "sharp";
import { SensorConfigurationDTO } from "../types/api";

type RedactedSensorEntity = Omit<SensorEntity, "owner" | "token">;
type RedactedSensorEntity = Omit<SensorEntity, "owner" | "readToken" | "writeToken">;

export default class SensorEntity {
constructor(
Expand All @@ -13,7 +13,8 @@ export default class SensorEntity {
public lowerThreshold: number, // relative to fieldCapacity
public upperThreshold: number,
public owner: number,
public token: string
public writeToken: string,
public readToken: string,
) {}

public static async fromDTO(
Expand Down
87 changes: 76 additions & 11 deletions ESPlant-Server/api/middlewares/authenticated.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
import { RequestHandler } from "express";
import SensorRepository from "../repositories/SensorRepository.js";

export const isAuthenticated: RequestHandler = (req, res, next) => {
if (!req.isAuthenticated()) {
export const isUser: RequestHandler = (req, res, next) => {
if (req.user == undefined) {
return res.status(401).send({
message: "unauthorized",
message: "not authenticated",
});
}
return next();
};

export const isUser: RequestHandler = (req, res, next) => {
if (req.user?.kind != "user") {
return res.status(403).send({
message: "unauthorized",
message: "user not logged in",
});
}

return next();
};

export const isSensor: RequestHandler = (req, res, next) => {
if (req.user?.kind != "sensor") {
export const isSensorWrite: RequestHandler = (req, res, next) => {
if (req.user == undefined) {
return res.status(401).send({
message: "not authenticated",
});
}

if (req.user?.kind != "sensor-write") {
return res.status(403).send({
message: "unauthorized",
message: "missing write token",
});
}

return next();
};

export const isOwner: RequestHandler = async (req, res, next) => {
if (req.user == undefined) {
return res.status(401).send({
message: "not authenticated",
});
}

if (req.user?.kind != "user") {
return res.status(403).send({
message: "user not logged in",
});
}

if (req.params.sensorId == undefined) {
throw new Error(
"Illegal usage of isOwner middleware - sensorId parameter missing"
Expand All @@ -45,8 +62,56 @@ export const isOwner: RequestHandler = async (req, res, next) => {

if (req.user?.userId != ownerId) {
return res.status(403).send({
message: "unauthorized",
message: "not an owner of this sensor",
});
}

return next();
};

export const isOwnerOrThisSensorRead: RequestHandler = async (req, res, next) => {
if (req.user == undefined) {
return res.status(401).send({
message: "not authenticated",
});
}

if (req.params.sensorId == undefined) {
throw new Error(
"Illegal usage of isOwnerOrSensorRead middleware - sensorId parameter missing"
);
}

const sensorId = parseInt(req.params.sensorId);

if (req.user?.kind == "sensor-read") {
if (req.user?.sensorId != sensorId) {
return res.status(403).send({
message: "wrong sensor for read token",
});
}

return next();
}

if (req.user?.kind == "user") {
const ownerId = await SensorRepository.getOwner(sensorId);
if (ownerId == undefined) {
return res.status(404).send({
message: "sensor not found",
});
}

if (req.user?.userId != ownerId) {
return res.status(403).send({
message: "not an owner of this sensor",
});
}

return next();
}

return res.status(403).send({
message: "not authenticated",
});
};
19 changes: 17 additions & 2 deletions ESPlant-Server/api/middlewares/passport.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Router } from "express";
import session from "express-session";
import KnexSessionStoreFactory from "connect-session-knex";
import passport from "../config/passport.js";
import passport, { AuthenticatedUser } from "../config/passport.js";
import { knex } from "../config/knex.js";
import { AuthenticateCallback } from "passport";

const SESSION_SECRET = process.env.SESSION_SECRET!;
if (SESSION_SECRET == undefined) {
Expand All @@ -27,7 +28,21 @@ router.use(
}),
})
);
router.use(passport.initialize());
router.use(passport.session());

// attempt to validate tokens on all routes
router.use((req, res, next) => {
const callback: AuthenticateCallback = (err, user, info, status) => {
if (err) {
return next(err)
}
if (user) {
req.user = user as AuthenticatedUser
}
// if authentication is not successful, do nothing - continue
return next()
};
passport.authenticate("bearer", { session: false }, callback)(req, res, next);
});

export default router;
9 changes: 8 additions & 1 deletion ESPlant-Server/api/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,14 @@ const migrations = [
// rename new table
"ALTER TABLE data_new RENAME TO data;",
],
}
},
{
name: "add_sensor_read_token",
statements: [
`ALTER TABLE sensor RENAME COLUMN token TO writeToken;`,
`ALTER TABLE sensor ADD COLUMN readToken TEXT;`
],
},
];

export async function migrateDatabase() {
Expand Down
Loading

0 comments on commit d6587bd

Please sign in to comment.