Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
✨ User session management
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed Jul 26, 2019
1 parent 5405b73 commit 20c5089
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 33 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "staart-manager",
"version": "1.0.106",
"version": "1.0.107",
"main": "index.js",
"repository": "[email protected]:AnandChowdhary/staart.git",
"author": "Anand Chowdhary <[email protected]>",
Expand Down Expand Up @@ -135,5 +135,5 @@
"setup"
],
"snyk": true,
"staart-version": "1.0.106"
"staart-version": "1.0.107"
}
2 changes: 1 addition & 1 deletion src/controllers/v1/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class AuthController {
async getImpersonate(req: Request, res: Response) {
const tokenUserId = res.locals.token.id;
const impersonateUserId = req.params.id;
res.json(await impersonate(tokenUserId, impersonateUserId));
res.json(await impersonate(tokenUserId, impersonateUserId, res.locals));
}

@Post("approve-location")
Expand Down
55 changes: 54 additions & 1 deletion src/controllers/v1/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
updateAccessTokenForUser,
getUserAccessTokenForUser,
createAccessTokenForUser,
getUserAccessTokensForUser
getUserAccessTokensForUser,
deleteSessionForUser,
getUserSessionForUser,
getUserSessionsForUser
} from "../../rest/user";
import {
Get,
Expand Down Expand Up @@ -451,4 +454,54 @@ export class UserController {
)
);
}

@Get(":id/sessions")
async getUserSessions(req: Request, res: Response) {
const id = await userUsernameToId(req.params.id, res.locals.token.id);
joiValidate(
{ id: [Joi.string().required(), Joi.number().required()] },
{ id }
);
const sessionParams = { ...req.query };
joiValidate(
{
start: Joi.string(),
itemsPerPage: Joi.number()
},
sessionParams
);
res.json(
await getUserSessionsForUser(res.locals.token.id, id, sessionParams)
);
}

@Get(":id/sessions/:sessionId")
async getUserSession(req: Request, res: Response) {
const id = await userUsernameToId(req.params.id, res.locals.token.id);
const sessionId = req.params.sessionId;
joiValidate(
{
id: [Joi.string().required(), Joi.number().required()],
sessionId: Joi.number().required()
},
{ id, sessionId }
);
res.json(await getUserSessionForUser(res.locals.token.id, id, sessionId));
}

@Delete(":id/sessions/:sessionId")
async deleteUserSession(req: Request, res: Response) {
const id = await userUsernameToId(req.params.id, res.locals.token.id);
const sessionId = req.params.sessionId;
joiValidate(
{
id: [Joi.string().required(), Joi.number().required()],
sessionId: Joi.number().required()
},
{ id, sessionId }
);
res.json(
await deleteSessionForUser(res.locals.token.id, id, sessionId, res.locals)
);
}
}
104 changes: 103 additions & 1 deletion src/crud/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
User,
ApprovedLocation,
BackupCode,
AccessToken
AccessToken,
Session
} from "../interfaces/tables/user";
import {
capitalizeFirstAndLastLetter,
Expand All @@ -30,6 +31,10 @@ import randomInt from "random-int";
import { getPaginatedData } from "./data";
import { accessToken, invalidateToken } from "../helpers/jwt";
import { TOKEN_EXPIRY_API_KEY_MAX } from "../config";
import {
addLocationToSession,
addLocationToSessions
} from "../helpers/location";

/**
* Get a list of all ${tableName("users")}
Expand Down Expand Up @@ -304,6 +309,7 @@ export const getUserBackupCode = async (userId: number, backupCode: number) => {
)
))[0];
};

/**
* Get a list of all approved locations of a user
*/
Expand Down Expand Up @@ -385,3 +391,99 @@ export const deleteAccessToken = async (
[accessTokenId, userId]
);
};

/**
* Get a list of all valid sessions of a user
*/
export const getUserSessions = async (userId: number, query: KeyValue) => {
const data = await getPaginatedData({
table: "sessions",
conditions: {
userId
},
...query
});
data.data.forEach((item, index) => {
delete data.data[index].jwtToken;
});
data.data = await addLocationToSessions(data.data);
return data;
};

/**
* Get a session
*/
export const getSession = async (userId: number, sessionId: number) => {
const data = await addLocationToSession(
(<Session[]>(
await query(
`SELECT * FROM ${tableName(
"sessions"
)} WHERE id = ? AND userId = ? LIMIT 1`,
[sessionId, userId]
)
))[0]
);
if (data) delete data.jwtToken;
return data;
};

/**
* Create a session
*/
export const createSession = async (newSession: Session) => {
newSession.createdAt = new Date();
newSession.updatedAt = newSession.createdAt;
return await query(
`INSERT INTO ${tableName("sessions")} ${tableValues(newSession)}`,
Object.values(newSession)
);
};

/**
* Update a user's details
*/
export const updateSession = async (
userId: number,
sessionId: number,
data: KeyValue
) => {
data.updatedAt = new Date();
data = removeReadOnlyValues(data);
return await query(
`UPDATE ${tableName("sessions")} SET ${setValues(
data
)} WHERE id = ? AND userId = ?`,
[...Object.values(data), sessionId, userId]
);
};

/**
* Update a user's details
*/
export const updateSessionByJwt = async (
userId: number,
sessionJwt: string,
data: KeyValue
) => {
data.updatedAt = new Date();
data = removeReadOnlyValues(data);
return await query(
`UPDATE ${tableName("sessions")} SET ${setValues(
data
)} WHERE jwtToken = ? AND userId = ?`,
[...Object.values(data), sessionJwt, userId]
);
};

/**
* Invalidate a session
*/
export const deleteSession = async (userId: number, sessionId: number) => {
const currentSession = await getSession(userId, sessionId);
if (currentSession.jwtToken) await invalidateToken(currentSession.jwtToken);
return await query(
`DELETE FROM ${tableName("sessions")} WHERE id = ? AND userId = ? LIMIT 1`,
[sessionId, userId]
);
};
43 changes: 26 additions & 17 deletions src/helpers/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import {
removeFalsyValues,
includesDomainInCommaList
} from "./utils";
import { checkApprovedLocation } from "../crud/user";
import {
checkApprovedLocation,
createSession,
updateSessionByJwt
} from "../crud/user";
import { Locals } from "../interfaces/general";
import { createEvent } from "../crud/event";
import {
getUserVerifiedEmails,
getUserPrimaryEmail,
Expand Down Expand Up @@ -165,16 +168,31 @@ export const approveLocationToken = (id: number, ipAddress: string) =>
export const refreshToken = (id: number) =>
generateToken({ id }, TOKEN_EXPIRY_REFRESH, Tokens.REFRESH);

export const postLoginTokens = async (user: User) => {
export const postLoginTokens = async (
user: User,
locals: Locals,
refreshTokenString?: string
) => {
if (!user.id) throw new Error(ErrorCode.USER_NOT_FOUND);
const refresh = await refreshToken(user.id);
if (!refreshTokenString) {
await createSession({
userId: user.id,
jwtToken: refresh,
ipAddress: locals.ipAddress || "unknown-ip-address",
userAgent: locals.userAgent || "unknown-user-agent"
});
} else {
await updateSessionByJwt(user.id, refreshTokenString, {});
}
return {
token: await loginToken(
deleteSensitiveInfoUser({
...user,
email: await getUserBestEmail(user.id)
})
),
refresh: await refreshToken(user.id)
refresh: !refreshTokenString ? refresh : undefined
};
};

Expand All @@ -189,9 +207,9 @@ export interface LoginResponse {
*/
export const getLoginResponse = async (
user: User,
type?: EventType,
strategy?: string,
locals?: Locals
type: EventType,
strategy: string,
locals: Locals
): Promise<LoginResponse> => {
if (!user.id) throw new Error(ErrorCode.USER_NOT_FOUND);
const verifiedEmails = await getUserVerifiedEmails(user);
Expand All @@ -213,20 +231,11 @@ export const getLoginResponse = async (
throw new Error(ErrorCode.UNAPPROVED_LOCATION);
}
}
if (type && strategy && locals)
await createEvent(
{
userId: user.id,
type,
data: { strategy }
},
locals
);
if (user.twoFactorEnabled)
return {
twoFactorToken: await twoFactorToken(user)
};
return await postLoginTokens(user);
return await postLoginTokens(user, locals);
};

const client = createHandyClient({
Expand Down
15 changes: 15 additions & 0 deletions src/helpers/location.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import maxmind, { CityResponse } from "maxmind";
import geoLite2 from "geolite2";
import { Event } from "../interfaces/tables/events";
import { Session } from "../interfaces/tables/user";
import { getItemFromCache, storeItemInCache } from "./cache";
import { CacheCategories } from "../interfaces/enum";

Expand Down Expand Up @@ -54,3 +55,17 @@ export const addLocationToEvent = async (event: Event) => {
}
return event;
};

export const addLocationToSessions = async (sessions: Session[]) => {
for await (let session of sessions) {
session = await addLocationToSession(session);
}
return sessions;
};

export const addLocationToSession = async (session: Session) => {
if (session.ipAddress) {
session.location = await getGeolocationFromIp(session.ipAddress);
}
return session;
};
5 changes: 4 additions & 1 deletion src/interfaces/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,10 @@ export enum UserScopes {
CREATE_USER_EMAILS = "user:emails:create",
READ_USER_EMAILS = "user:emails:read",
DELETE_USER_EMAILS = "user:emails:delete",
RESEND_USER_EMAIL_VERIFICATION = "user:emails:resend-verification"
RESEND_USER_EMAIL_VERIFICATION = "user:emails:resend-verification",
CREATE_USER_SESSION = "user:sessions:create",
READ_USER_SESSION = "user:sessions:read",
DELETE_USER_SESSION = "user:sessions:delete"
}

export enum Webhooks {
Expand Down
9 changes: 9 additions & 0 deletions src/interfaces/tables/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Tokens
} from "../enum";
import { IdRow, Row } from "../general";
import { GeoLocation } from "../../helpers/location";

export interface User extends IdRow {
name: string;
Expand Down Expand Up @@ -66,3 +67,11 @@ export interface AccessTokenResponse {
sub: Tokens;
exp: number;
}

export interface Session extends IdRow {
userId: number;
jwtToken: string;
ipAddress: string;
userAgent: string;
location?: GeoLocation;
}
Loading

0 comments on commit 20c5089

Please sign in to comment.