Restructure access token model to store user ID
Signed-off-by: Aelita4 <kontakt@mikorosa.pl>
This commit is contained in:
		
							parent
							
								
									d3ac213590
								
							
						
					
					
						commit
						eafea0c1f5
					
				| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import { AccessTokens } from './mongodb';
 | 
			
		||||
import type AccessToken from '../../types/AccessToken';
 | 
			
		||||
import { createHash } from 'crypto';
 | 
			
		||||
import { ObjectId } from 'mongodb';
 | 
			
		||||
 | 
			
		||||
export const createAccessToken = async (accessToken: AccessToken) => {
 | 
			
		||||
    const newAccessToken = await (await AccessTokens()).insertOne(accessToken);
 | 
			
		||||
| 
						 | 
				
			
			@ -12,7 +13,7 @@ export const getAccessToken = async (accessToken: string) => {
 | 
			
		|||
    
 | 
			
		||||
    const type = accessToken.split(".")[0];
 | 
			
		||||
    const createdAt = new Date(Number(Buffer.from(accessToken.split(".")[1], 'base64url').toString()));
 | 
			
		||||
    const username = Buffer.from(accessToken.split(".")[2], 'base64url').toString();
 | 
			
		||||
    const user = new ObjectId(Buffer.from(accessToken.split(".")[2], 'base64url').toString());
 | 
			
		||||
    const random = accessToken.split(".")[3];
 | 
			
		||||
    const entropy = createHash("sha256").update(random).digest("hex");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +21,7 @@ export const getAccessToken = async (accessToken: string) => {
 | 
			
		|||
    return accessTokens.findOne({ $and: [
 | 
			
		||||
        { type },
 | 
			
		||||
        { createdAt },
 | 
			
		||||
        { username },
 | 
			
		||||
        { user },
 | 
			
		||||
        { entropy }
 | 
			
		||||
    ] }) as Promise<AccessToken | null>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +29,7 @@ export const getAccessToken = async (accessToken: string) => {
 | 
			
		|||
export const getAllAccessTokens = async () => {
 | 
			
		||||
    const master: AccessToken = {
 | 
			
		||||
        type: "X",
 | 
			
		||||
        username: "0",
 | 
			
		||||
        user: null,
 | 
			
		||||
        entropy: "b70972be3921e04a8d0c442f64c2c19a4ac01e886e8e5649916f9b88870fa6dd",
 | 
			
		||||
        createdAt: new Date(1698851542427),
 | 
			
		||||
        expiresAt: null,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,8 @@ import { Users } from '../db/mongodb';
 | 
			
		|||
import type User from '../../types/User';
 | 
			
		||||
import type Resources from '../../types/Resources';
 | 
			
		||||
import type Building from '../../types/Building';
 | 
			
		||||
import type AccessToken from '../../types/AccessToken';
 | 
			
		||||
import { ObjectId } from 'mongodb';
 | 
			
		||||
 | 
			
		||||
export const getAllUsers = async () => {
 | 
			
		||||
    const users = await Users();
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +15,13 @@ export const createUser = async (user: User) => {
 | 
			
		|||
    return newUser;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getUserById = async (id: ObjectId) => {
 | 
			
		||||
    const users = await Users();
 | 
			
		||||
    return users.findOne({
 | 
			
		||||
        _id: id
 | 
			
		||||
    }) as Promise<User | null>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getUserByNickOrEmail = async (searchString: string) => {
 | 
			
		||||
    const users = await Users();
 | 
			
		||||
    return users.findOne({
 | 
			
		||||
| 
						 | 
				
			
			@ -23,6 +32,13 @@ export const getUserByNickOrEmail = async (searchString: string) => {
 | 
			
		|||
    }) as Promise<User | null>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getUserByAccessToken = async(accessToken: string | AccessToken): Promise<User | null> => {
 | 
			
		||||
    if(typeof accessToken === "string") {
 | 
			
		||||
        const userId = new ObjectId(Buffer.from(accessToken.split(".")[2], 'base64url').toString());
 | 
			
		||||
        return getUserById(userId);
 | 
			
		||||
    } else return getUserById(accessToken.user as ObjectId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getUserResources = async (username: string): Promise<Resources> => {
 | 
			
		||||
    const users = await Users();
 | 
			
		||||
    const user = await users.findOne({ username });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,7 @@
 | 
			
		|||
import type { ObjectId } from "mongodb";
 | 
			
		||||
import type AccessToken from "../../types/AccessToken";
 | 
			
		||||
import { getAccessToken } from "../db/accessTokens";
 | 
			
		||||
import { getUserById } from "../db/users";
 | 
			
		||||
 | 
			
		||||
export default async function validateAccessToken(request: Request): Promise<Response | AccessToken> {
 | 
			
		||||
    let accessToken = request.url.split("?")[1]?.split("&").filter((x) => x.split("=")[0] === "token")[0].split("=")[1];
 | 
			
		||||
| 
						 | 
				
			
			@ -30,12 +32,21 @@ export default async function validateAccessToken(request: Request): Promise<Res
 | 
			
		|||
            error: "Invalid Access Token"
 | 
			
		||||
        }), { status: 401 }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const user = await getUserById(response.user as ObjectId);
 | 
			
		||||
    if(!user) return new Response (
 | 
			
		||||
        JSON.stringify({
 | 
			
		||||
            code: 404,
 | 
			
		||||
            message: "Not found",
 | 
			
		||||
            data: "Access token does not match any user"
 | 
			
		||||
        }), { status: 404 }
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    if(response.createdAt.getTime() > Date.now()) return new Response(
 | 
			
		||||
        JSON.stringify({
 | 
			
		||||
            code: 403,
 | 
			
		||||
            message: "Forbidden",
 | 
			
		||||
            data: "Access token is invalid for user " + response.username + ", are you travelling in time?"
 | 
			
		||||
            data: "Access token is invalid for user " + user.username + ", are you travelling in time?"
 | 
			
		||||
        }), { status: 403 }
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +54,7 @@ export default async function validateAccessToken(request: Request): Promise<Res
 | 
			
		|||
        JSON.stringify({
 | 
			
		||||
            code: 403,
 | 
			
		||||
            message: "Forbidden",
 | 
			
		||||
            data: "Access token is invalid for user " + response.username + ", token expired"
 | 
			
		||||
            data: "Access token is invalid for user " + user.username + ", token expired"
 | 
			
		||||
        }), { status: 403 }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
---
 | 
			
		||||
import Layout from "../../layouts/Layout.astro";
 | 
			
		||||
import { getAllAccessTokens } from "../../lib/db/accessTokens";
 | 
			
		||||
import { getUserByAccessToken } from "../../lib/db/users";
 | 
			
		||||
 | 
			
		||||
const tokens = await getAllAccessTokens();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,13 +27,13 @@ const type = {
 | 
			
		|||
            </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
            {tokens.map(token => <tr>
 | 
			
		||||
            {tokens.map(async token => <tr>
 | 
			
		||||
                <td>{token._id}</td>
 | 
			
		||||
                <td>{type[token.type] ?? `Other`}</td>
 | 
			
		||||
                <td>{token.username}</td>
 | 
			
		||||
                <td>{(await getUserByAccessToken(token))?.username}</td>
 | 
			
		||||
                <td>{token.entropy}</td>
 | 
			
		||||
                <td>{token.createdAt.toISOString().slice(0, 19).replace(/-/g, "/").replace("T", " ")}</td>
 | 
			
		||||
                <td>{token.expiresAt?.toISOString().slice(0, 19).replace(/-/g, "/").replace("T", " ") ?? "null"}</td>
 | 
			
		||||
                <td>{token.expiresAt?.toISOString().slice(0, 19).replace(/-/g, "/").replace("T", " ") ?? "Never"}</td>
 | 
			
		||||
                <td>{token.createdFrom}</td>
 | 
			
		||||
                <td>{token.expiresAt !== null && Date.now() > token.expiresAt.getTime() ? <span style="color: red;">Expired {Math.floor((Date.now() - token.expiresAt.getTime()) / 1000 / 60)} minutes ago</span> : Date.now() < token.createdAt.getTime() ? <span style="color: cyan;">TimeTravelException (check in {Math.floor((token.createdAt.getTime() - Date.now()) / 1000 / 60)} minutes)</span> : <span style="color: lime;">Valid for{token.expiresAt === null ? "ever" : ` ${Math.floor((token.expiresAt.getTime() - Date.now()) / 1000 / 60)} minutes`}</span>}</td>
 | 
			
		||||
            </tr>)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,8 @@ import { randomBytes, createHash } from "crypto";
 | 
			
		|||
import type { APIRoute } from "astro";
 | 
			
		||||
import type AccessToken from "../../../types/AccessToken";
 | 
			
		||||
import { createAccessToken } from "../../../lib/db/accessTokens";
 | 
			
		||||
import { getUserByNickOrEmail } from "../../../lib/db/users";
 | 
			
		||||
import type { ObjectId } from "mongodb";
 | 
			
		||||
 | 
			
		||||
export const POST: APIRoute = async({ request }) => {
 | 
			
		||||
    const data = await request.json().catch(() => {return new Response(
 | 
			
		||||
| 
						 | 
				
			
			@ -9,7 +11,7 @@ export const POST: APIRoute = async({ request }) => {
 | 
			
		|||
            code: 400,
 | 
			
		||||
            message: "Bad Request",
 | 
			
		||||
            error: "Invalid JSON"
 | 
			
		||||
        })
 | 
			
		||||
        }), { status: 400 }
 | 
			
		||||
    )});
 | 
			
		||||
 | 
			
		||||
    if(!data.username) return new Response(
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +19,7 @@ export const POST: APIRoute = async({ request }) => {
 | 
			
		|||
            code: 400,
 | 
			
		||||
            message: "Bad Request",
 | 
			
		||||
            error: "Username is required"
 | 
			
		||||
        })
 | 
			
		||||
        }), { status: 400 }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const header = request.headers.get("Authorization");
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +30,7 @@ export const POST: APIRoute = async({ request }) => {
 | 
			
		|||
            code: 401,
 | 
			
		||||
            message: "Unauthorized",
 | 
			
		||||
            error: "Access Token is required"
 | 
			
		||||
        })
 | 
			
		||||
        }), { status: 401 }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if(token !== import.meta.env.MASTER_ACCESSTOKEN) return new Response(
 | 
			
		||||
| 
						 | 
				
			
			@ -36,21 +38,30 @@ export const POST: APIRoute = async({ request }) => {
 | 
			
		|||
            code: 401,
 | 
			
		||||
            message: "Unauthorized",
 | 
			
		||||
            error: "Invalid Access Token"
 | 
			
		||||
        })
 | 
			
		||||
        }), { status: 401 }
 | 
			
		||||
    )
 | 
			
		||||
    else {
 | 
			
		||||
        const userFromDb = await getUserByNickOrEmail(data.username);
 | 
			
		||||
        if(!userFromDb) return new Response(
 | 
			
		||||
            JSON.stringify({
 | 
			
		||||
                code: 404,
 | 
			
		||||
                message: "Not found",
 | 
			
		||||
                error: `User ${data.username} not found`
 | 
			
		||||
            }), { status: 404 }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const now = new Date();
 | 
			
		||||
        const timestamp = Buffer.from(String(Date.now())).toString('base64url');
 | 
			
		||||
        const username = Buffer.from(data.username).toString('base64url');
 | 
			
		||||
        const user = Buffer.from(userFromDb._id?.toString() ?? "").toString('base64url');
 | 
			
		||||
        const random = randomBytes(16).toString("base64url");
 | 
			
		||||
        const randomHashed = createHash("sha256").update(random).digest("hex");
 | 
			
		||||
        const expiresIn = (data.duration ?? 86400) * 1000;
 | 
			
		||||
 | 
			
		||||
        const tokenString = `A.${timestamp}.${username}.${random}`;
 | 
			
		||||
        const tokenString = `A.${timestamp}.${user}.${random}`;
 | 
			
		||||
 | 
			
		||||
        const accessToken: AccessToken = {
 | 
			
		||||
            type: "A",
 | 
			
		||||
            username: data.username,
 | 
			
		||||
            user: userFromDb._id as ObjectId,
 | 
			
		||||
            entropy: randomHashed.toString(),
 | 
			
		||||
            createdAt: now,
 | 
			
		||||
            expiresAt: new Date(now.getTime() + expiresIn),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,19 @@
 | 
			
		|||
import type { APIRoute } from "astro";
 | 
			
		||||
import validateAccessToken from "../../../lib/utils/validateAccessToken";
 | 
			
		||||
import { getAccessToken } from "../../../lib/db/accessTokens";
 | 
			
		||||
import { getUserByAccessToken } from "../../../lib/db/users";
 | 
			
		||||
import type User from "../../../types/User";
 | 
			
		||||
 | 
			
		||||
export const GET: APIRoute = async({ request }) => {
 | 
			
		||||
    const response = await validateAccessToken(request);
 | 
			
		||||
    if(response instanceof Response) return response;
 | 
			
		||||
 | 
			
		||||
    const user = (await getUserByAccessToken(response)) as User;
 | 
			
		||||
 | 
			
		||||
    return new Response(
 | 
			
		||||
        JSON.stringify({
 | 
			
		||||
            code: 200,
 | 
			
		||||
            message: "OK",
 | 
			
		||||
            data: "Access token valid for user " + response.username
 | 
			
		||||
            data: "Access token valid for user " + user.username
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import type { ObjectId } from "mongodb";
 | 
			
		|||
export default interface AccessToken {
 | 
			
		||||
    _id?: ObjectId;
 | 
			
		||||
    type: "A" | "X";
 | 
			
		||||
    username: string;
 | 
			
		||||
    user: ObjectId | null;
 | 
			
		||||
    entropy: string;
 | 
			
		||||
    createdAt: Date;
 | 
			
		||||
    expiresAt: Date | null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue