Skip to content
Snippets Groups Projects
Commit ce7eeeca authored by poka's avatar poka :speech_balloon:
Browse files

enh: refactor update_profile with types

parent 63ba7f15
No related branches found
No related tags found
No related merge requests found
import { Application, Context, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
import {
Application,
Context,
Router,
} from "https://deno.land/x/oak@v12.6.1/mod.ts";
import { Client } from "https://deno.land/x/postgres@v0.17.0/mod.ts";
import { load } from "https://deno.land/std@0.209.0/dotenv/mod.ts";
import { updateProfile } from "./lib/update_profile.ts";
import { isProfilesTableEmpty, runCsplusImport, waitForTableCreation } from "./lib/utils.ts";
import {
isProfilesTableEmpty,
runCsplusImport,
waitForTableCreation,
} from "./lib/utils.ts";
let dbUser, dbDatabase, dbPassword, dbHostname, importCsplusData;
const dbPort = 5432;
......@@ -32,24 +40,27 @@ const client = new Client({
port: dbPort,
});
await client.connect()
await client.connect();
const app = new Application();
const router = new Router();
// Wait for table creation before continue
await waitForTableCreation(client, 'public.profiles');
await waitForTableCreation(client, "public.profiles");
// Import Cs+ data
const profilesEmpty = await isProfilesTableEmpty(client);
if (profilesEmpty && importCsplusData) {
await runCsplusImport(isProduction)
await runCsplusImport(isProduction);
}
// Manage routes
router.post("/update-profile-data", async (ctx: Context) => await updateProfile(ctx, client));
router.post(
"/update-profile-data",
async (ctx: Context) => await updateProfile(ctx, client),
);
app.use(router.routes());
app.use(router.allowedMethods());
console.log("\nDatapod is started")
console.log("\nDatapod is started");
await app.listen({ port: 3000 });
import { signatureVerify, base64Decode } from 'https://deno.land/x/polkadot@0.2.44/util-crypto/mod.ts';
import {
base64Decode,
signatureVerify,
} from "https://deno.land/x/polkadot@0.2.44/util-crypto/mod.ts";
import { Profile } from "./types.ts";
export enum SignatureResponse {
valid,
invalidHash,
invalidSignature
invalidSignature,
}
export async function verifySignature(address: string, signatureBase64: string, hash: string, playload: string): Promise<SignatureResponse> {
export async function verifySignature(
bodyValue: Profile,
): Promise<SignatureResponse> {
const { description, avatarBase64, geoloc, title, city, socials } = bodyValue;
const payload = JSON.stringify({
description,
avatarBase64,
geoloc,
title,
city,
socials,
});
try {
const hashVerify = await createHashedMessage(playload);
if (hash != hashVerify) {
console.error('hash documents is invalid')
const hashVerify = await createHashedMessage(payload);
if (bodyValue.hash != hashVerify) {
console.error("hash documents is invalid");
return SignatureResponse.invalidHash;
}
const messageUint8Array = new TextEncoder().encode(hash);
const signature = base64Decode(signatureBase64);
const signedMessage = signatureVerify(messageUint8Array, signature, address)
const messageUint8Array = new TextEncoder().encode(bodyValue.hash);
const signature = base64Decode(bodyValue.signature);
const signedMessage = signatureVerify(
messageUint8Array,
signature,
bodyValue.address,
);
return signedMessage.isValid ? SignatureResponse.valid : SignatureResponse.invalidSignature;
return signedMessage.isValid
? SignatureResponse.valid
: SignatureResponse.invalidSignature;
} catch (error) {
console.error('Signature verification failed:', error);
console.error("Signature verification failed:", error);
throw new Error(`Cannot verify signature`);
}
}
......@@ -31,11 +53,12 @@ async function createHashedMessage(messageToSign: string): Promise<string> {
const data = encoder.encode(messageToSign);
// Calculate SHA-256
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
// Convert to hexa
const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase();
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0"))
.join("").toUpperCase();
return hashHex;
}
type Geolocation = { latitude: number | null; longitude: number | null };
type SocialInfo = { url: string; type: string };
export type Profile = {
address: string;
hash: string;
signature: string;
avatarBase64?: string | null;
description?: string;
geoloc?: Geolocation;
title?: string;
city?: string;
socials?: SocialInfo[];
};
......@@ -2,30 +2,43 @@ import { Context } from "https://deno.land/x/oak@v12.6.1/context.ts";
import { Client } from "https://deno.land/x/postgres@v0.17.0/client.ts";
import { SignatureResponse, verifySignature } from "./signature_verify.ts";
import { convertBase64ToBytea } from "./utils.ts";
import { Profile } from "./types.ts";
export async function updateProfile(ctx: Context, client: Client) {
try {
const body = (await ctx.request.body().value);
const bodyValue = body.variables ? body.variables : (body.input ? body.input : {});
const { address, hash, signature, avatarBase64, description, geoloc, title, city, socials } = bodyValue;
const body = await ctx.request.body().value;
const profile: Profile = body.variables || body.input || {};
const socialJson = JSON.stringify(profile.socials);
const socialJson = JSON.stringify(socials);
// Validate input
if (!profile.address || !profile.hash || !profile.signature) {
ctx.response.status = 400;
ctx.response.body = {
success: false,
message: "Address, hash, and signature are required.",
};
return;
}
// Verify signature
const playload = JSON.stringify({description, avatarBase64, geoloc, title, city, socials});
const signatureResult = await verifySignature(address, signature, hash, playload);
const signatureResult = await verifySignature(profile);
if (signatureResult != SignatureResponse.valid) {
ctx.response.status = 401;
console.error('Invalid signature: ' + SignatureResponse[signatureResult])
ctx.response.body = { success: false, message: 'Invalid signature: ' + SignatureResponse[signatureResult]};
console.error("Invalid signature: " + SignatureResponse[signatureResult]);
ctx.response.body = {
success: false,
message: "Invalid signature: " + SignatureResponse[signatureResult],
};
return;
}
console.log('Signature is valid')
console.log("Signature is valid");
// Convert avatar to bytes
const avatarBytea = avatarBase64 ? convertBase64ToBytea(avatarBase64) : null;
const avatarBytea = convertBase64ToBytea(
profile.avatarBase64,
);
// Update the user profile
// Prepare and execute database query
const query = `
INSERT INTO profiles (address, description, avatar, geoloc, title, city, socials, created_at, updated_at)
VALUES ($1, $2, $3, point($4, $5), $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
......@@ -42,22 +55,37 @@ export async function updateProfile(ctx: Context, client: Client) {
try {
await client.queryObject({
text: query,
args: [address, description, avatarBytea, geoloc ? geoloc["latitude"] : null, geoloc ? geoloc["longitude"] : null, title, city, socialJson],
args: [
profile.address,
profile.description,
avatarBytea,
profile.geoloc?.latitude,
profile.geoloc?.longitude,
profile.title,
profile.city,
socialJson,
],
});
console.log(`Profile ${address} has been updated`);
} catch (error) {
throw error;
}
// Success response
ctx.response.status = 200;
console.log(`Profile ${profile.address} has been updated`);
ctx.response.body = {
success: true,
message: `Profile ${address} has been updated`
message: `Profile ${profile.address} has been updated`,
};
} catch (error) {
console.error("Database error in updating profile:", error);
ctx.response.status = 500;
ctx.response.body = {
success: false,
message: "Internal server error :" + error,
};
}
} catch (error) {
console.error('Error updating profile:', error);
console.error("Error updating profile:", error);
ctx.response.status = 500;
ctx.response.body = { success: false, message: 'Error updating user' };
ctx.response.body = {
success: false,
message: "Error updating user: " + error,
};
}
}
import { Client } from "https://deno.land/x/postgres@v0.17.0/client.ts";
export function convertBase64ToBytea(base64String: string): Uint8Array {
export function convertBase64ToBytea(
base64String: string | null | undefined,
): Uint8Array | null {
if (base64String == null || base64String == undefined) {
return null;
}
// Remove the MIME type prefix from the base64 string, if present
const base64Data = base64String.split(',')[1] || base64String;
const base64Data = base64String.split(",")[1] || base64String;
// Convert the base64 string to a binary string
const binaryString = atob(base64Data);
......@@ -17,23 +23,36 @@ export function convertBase64ToBytea(base64String: string): Uint8Array {
}
export async function runCsplusImport(isProduction: boolean) {
const command = new Deno.Command("./migrate_csplus/target/release/migrate_csplus", {
const command = new Deno.Command(
"./migrate_csplus/target/release/migrate_csplus",
{
env: { "PRODUCTION": isProduction.toString() },
stdout: "piped",
stderr: "piped",
});
},
);
const process = command.spawn();
process.stdout.pipeTo(Deno.stdout.writable, { preventClose: true, preventCancel: true, preventAbort: true});
process.stderr.pipeTo(Deno.stderr.writable, { preventClose: true, preventCancel: true, preventAbort: true});
process.stdout.pipeTo(Deno.stdout.writable, {
preventClose: true,
preventCancel: true,
preventAbort: true,
});
process.stderr.pipeTo(Deno.stderr.writable, {
preventClose: true,
preventCancel: true,
preventAbort: true,
});
await process.status;
console.log("End of Cs+ data import")
console.log("End of Cs+ data import");
}
export async function isProfilesTableEmpty(client: Client): Promise<boolean> {
const result = await client.queryObject<{ count: bigint }>('SELECT COUNT(*) FROM public.profiles');
const result = await client.queryObject<{ count: bigint }>(
"SELECT COUNT(*) FROM public.profiles",
);
return result.rows[0].count === 0n;
}
......@@ -41,9 +60,14 @@ interface TableCheckResult {
to_regclass: string | null;
}
async function checkTableExists(client: Client, tableName: string): Promise<boolean> {
async function checkTableExists(
client: Client,
tableName: string,
): Promise<boolean> {
try {
const result = await client.queryObject<TableCheckResult>(`SELECT to_regclass('${tableName}')`);
const result = await client.queryObject<TableCheckResult>(
`SELECT to_regclass('${tableName}')`,
);
return result.rows[0].to_regclass !== null;
} catch (error) {
console.error("Error checking table existence:", error);
......@@ -51,10 +75,14 @@ async function checkTableExists(client: Client, tableName: string): Promise<bool
}
}
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function waitForTableCreation(client: Client, tableName: string, maxAttempts = 10) {
export async function waitForTableCreation(
client: Client,
tableName: string,
maxAttempts = 10,
) {
let attempts = 0;
while (attempts < maxAttempts) {
if (await checkTableExists(client, tableName)) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment