feat: Add Microsoft Azure AD authentication environment variables to .env.example (#115)

* Apply patch [skip ci]

* Empty commit to trigger CI

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

* Apply patch [skip ci]

---------

Co-authored-by: open-swe-dev-andrinoid[bot] <open-swe-dev-andrinoid@users.noreply.github.com>
This commit is contained in:
open-swe-dev-andrinoid[bot] 2026-01-28 10:47:50 -06:00 committed by GitHub
parent 1df84f59a4
commit 744115d230
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 243 additions and 49 deletions

View File

@ -27,3 +27,48 @@ STRIPE_WEBHOOK_SECRET=""
# Get this from your Stripe Dashboard: https://dashboard.stripe.com/apikeys # Get this from your Stripe Dashboard: https://dashboard.stripe.com/apikeys
# Example: pk_test_51AbCdEfGhIjKlMnOpQrStUvWxYz1234567890 # Example: pk_test_51AbCdEfGhIjKlMnOpQrStUvWxYz1234567890
STRIPE_PUBLISHABLE_KEY="" STRIPE_PUBLISHABLE_KEY=""
# Microsoft Azure AD Authentication
# AUTHORITY: Microsoft authority URL for authentication
# Format: https://login.microsoftonline.com/{tenantId}
# Get tenantId from Azure Portal: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
# Example: https://login.microsoftonline.com/8a8b8c8d-9e9f-0a0b-1c2d-3e4f5a6b7c8d
AUTHORITY=""
# CLIENT_ID: Microsoft application client ID (from Azure AD app registration)
# Get this from Azure Portal: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
# Navigate to your app registration and copy the Application (client) ID
# Example: 12345678-1234-1234-1234-123456789012
CLIENT_ID=""
# CLIENT_SECRET: Microsoft application client secret (server-side use only)
# Get this from Azure Portal: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
# Navigate to your app registration > Certificates & secrets > New client secret
# IMPORTANT: This secret is used server-side only and should never be committed to version control
# Example: abcdefghijklmnopqrstuvwxyz1234567890
CLIENT_SECRET=""
# TENANT_ID: Microsoft Azure tenant ID
# Get this from Azure Portal: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade
# Navigate to your app registration and copy the Directory (tenant) ID
# Example: 8a8b8c8d-9e9f-0a0b-1c2d-3e4f5a6b7c8d
TENANT_ID=""
# REDIRECT_URI: OAuth redirect URI (must match Azure AD app registration)
# This must exactly match the Redirect URI configured in your Azure AD app registration
# For local development: http://localhost:8080/auth/callback
# For production: https://yourdomain.com/auth/callback
# Example: http://localhost:8080/auth/callback
REDIRECT_URI=""
# TOKEN_URL: Token endpoint URL for OAuth token exchange
# Format: https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
# Example: https://login.microsoftonline.com/8a8b8c8d-9e9f-0a0b-1c2d-3e4f5a6b7c8d/oauth2/v2.0/token
TOKEN_URL=""
# REDIRECT_HOME: Redirect home URL (base URL for the application)
# This is the base URL where users are redirected after authentication
# For local development: http://localhost:8080
# For production: https://yourdomain.com
# Example: http://localhost:8080
REDIRECT_HOME=""

3
deno.json Executable file → Normal file
View File

@ -46,7 +46,8 @@
"google-cloud-storage": "npm:@google-cloud/storage", "google-cloud-storage": "npm:@google-cloud/storage",
"gLog": "npm:google-logging-utils", "gLog": "npm:google-logging-utils",
"cosmos": "npm:@azure/cosmos", "cosmos": "npm:@azure/cosmos",
"@playwright/test": "npm:@playwright/test" "@playwright/test": "npm:@playwright/test",
"stripe": "npm:stripe@^17.3.0"
}, },
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": [

View File

@ -1,12 +1,27 @@
// routes/api/delete/lease_document.ts // routes/api/delete/lease_document.ts
import { define } from "../../../utils.ts"; import { define } from "../../../utils.ts";
import { deleteLeaseDocument } from "../../../services/leaseDocumentService.ts"; import {
deleteLeaseDocument,
getDocumentById,
} from "../../../services/leaseDocumentService.ts";
import { SessionCookie } from "../../auth/callback.ts"; import { SessionCookie } from "../../auth/callback.ts";
import {
getLeasesByOwner,
getLeasesByTenant,
} from "../../../services/leaseService.ts";
export const handler = define.handlers({ export const handler = define.handlers({
async POST(ctx) { async POST(ctx) {
try { try {
const stateData: SessionCookie = ctx.state.data; const stateData: SessionCookie = ctx.state.data;
const requestingUserId = stateData.user_id;
if (!requestingUserId) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const contentType = ctx.req.headers.get("content-type") || ""; const contentType = ctx.req.headers.get("content-type") || "";
let data: Record<string, string> = {}; let data: Record<string, string> = {};
@ -41,6 +56,37 @@ export const handler = define.handlers({
); );
} }
const document = await getDocumentById(id);
if (!document) {
return new Response(
JSON.stringify({
error: "Document not found",
}),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
const lease = await getLeasesByTenant(requestingUserId).then((leases) =>
leases.find((l) => l.id === document.lease_id)
);
const ownerLeases = await getLeasesByOwner(requestingUserId);
const isOwner = ownerLeases.some((l) => l.id === document.lease_id);
if (!lease && !isOwner) {
return new Response(
JSON.stringify({
error: "Access denied - you must be the tenant or property owner",
}),
{
status: 403,
headers: { "Content-Type": "application/json" },
},
);
}
const result = await deleteLeaseDocument(id); const result = await deleteLeaseDocument(id);
// Return appropriate response based on request type // Return appropriate response based on request type

View File

@ -1,12 +1,24 @@
// routes/api/delete/property_media.ts // routes/api/delete/property_media.ts
import { define } from "../../../utils.ts"; import { define } from "../../../utils.ts";
import { deletePropertyMedia } from "../../../services/propertyMediaService.ts"; import {
deletePropertyMedia,
getMediaByProperty,
} from "../../../services/propertyMediaService.ts";
import { SessionCookie } from "../../auth/callback.ts"; import { SessionCookie } from "../../auth/callback.ts";
import { getPropertiesByOwner } from "../../../services/propertyService.ts";
export const handler = define.handlers({ export const handler = define.handlers({
async POST(ctx) { async POST(ctx) {
try { try {
const stateData: SessionCookie = ctx.state.data; const stateData: SessionCookie = ctx.state.data;
const requestingUserId = stateData.user_id;
if (!requestingUserId) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const contentType = ctx.req.headers.get("content-type") || ""; const contentType = ctx.req.headers.get("content-type") || "";
let data: Record<string, string> = {}; let data: Record<string, string> = {};
@ -41,6 +53,34 @@ export const handler = define.handlers({
); );
} }
const media = await getMediaByProperty(id);
if (!media || media.length === 0) {
return new Response(
JSON.stringify({
error: "Media not found",
}),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
const property = await getPropertiesByOwner(requestingUserId);
const isOwner = property.some((p) => p.id === media[0].property_id);
if (!isOwner) {
return new Response(
JSON.stringify({
error: "Access denied - you must be the property owner",
}),
{
status: 403,
headers: { "Content-Type": "application/json" },
},
);
}
const result = await deletePropertyMedia(id); const result = await deletePropertyMedia(id);
// Return appropriate response based on request type // Return appropriate response based on request type

View File

@ -1,10 +1,21 @@
// routes/api/delete/users.ts // routes/api/delete/users.ts
import { define } from "../../../utils.ts"; import { define } from "../../../utils.ts";
import { deleteUser } from "../../../services/userService.ts"; import { deleteUser, getUser } from "../../../services/userService.ts";
import { SessionCookie } from "../../auth/callback.ts"; import { SessionCookie } from "../../auth/callback.ts";
export const handler = define.handlers({ export const handler = define.handlers({
async POST(ctx) { async POST(ctx) {
const stateData: SessionCookie = ctx.state.data;
const requestingUserId = stateData.user_id;
const requestingUserRole = stateData.role;
if (!requestingUserId) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const formData = await ctx.req.formData(); const formData = await ctx.req.formData();
const user_id = formData.get("user_id"); const user_id = formData.get("user_id");
const sender = formData.get("sender"); const sender = formData.get("sender");
@ -13,8 +24,37 @@ export const handler = define.handlers({
return new Response("Missing user_id or sender", { status: 400 }); return new Response("Missing user_id or sender", { status: 400 });
} }
const targetUserId = user_id.toString();
if (requestingUserId === targetUserId) {
return new Response(
JSON.stringify({ error: "Cannot delete your own account" }),
{
status: 403,
headers: { "Content-Type": "application/json" },
},
);
}
const targetUser = await getUser(targetUserId);
if (!targetUser) {
return new Response(JSON.stringify({ error: "User not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
const isAdmin = requestingUserRole.includes("admin") ||
targetUser.user_type === "admin";
if (!isAdmin) {
return new Response(JSON.stringify({ error: "Admin access required" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}
// Delete the user // Delete the user
await deleteUser(user_id.toString()); await deleteUser(targetUserId);
// Redirect back to the sender URL (must be absolute) // Redirect back to the sender URL (must be absolute)
const senderPath = sender.toString(); const senderPath = sender.toString();

View File

@ -1,10 +1,25 @@
// routes/api/delete/user_roles.ts // routes/api/delete/user_roles.ts
import { define } from "../../../utils.ts"; import { define } from "../../../utils.ts";
import { deleteUserRole } from "../../../services/userService.ts"; import {
deleteUserRole,
getAllRoles,
getUser,
} from "../../../services/userService.ts";
import { SessionCookie } from "../../auth/callback.ts"; import { SessionCookie } from "../../auth/callback.ts";
export const handler = define.handlers({ export const handler = define.handlers({
async POST(ctx) { async POST(ctx) {
const stateData: SessionCookie = ctx.state.data;
const requestingUserId = stateData.user_id;
const requestingUserRole = stateData.role;
if (!requestingUserId) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const formData = await ctx.req.formData(); const formData = await ctx.req.formData();
const user_id = formData.get("user_id"); const user_id = formData.get("user_id");
const sender = formData.get("sender"); const sender = formData.get("sender");
@ -13,7 +28,36 @@ export const handler = define.handlers({
return new Response("Missing user_id or sender", { status: 400 }); return new Response("Missing user_id or sender", { status: 400 });
} }
await deleteUserRole(user_id.toString()); const targetUserId = user_id.toString();
if (requestingUserId === targetUserId) {
return new Response(
JSON.stringify({ error: "Cannot remove your own role" }),
{
status: 403,
headers: { "Content-Type": "application/json" },
},
);
}
const targetUser = await getUser(targetUserId);
if (!targetUser) {
return new Response(JSON.stringify({ error: "User not found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
const isAdmin = requestingUserRole.includes("admin") ||
targetUser.user_type === "admin";
if (!isAdmin) {
return new Response(JSON.stringify({ error: "Admin access required" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}
await deleteUserRole(targetUserId);
// Redirect back to the sender URL (must be absolute) // Redirect back to the sender URL (must be absolute)
const senderPath = sender.toString(); const senderPath = sender.toString();

View File

@ -3,32 +3,7 @@ import {
createPaymentTransaction, createPaymentTransaction,
getTransactionByStripeId, getTransactionByStripeId,
} from "../../../services/paymentService.ts"; } from "../../../services/paymentService.ts";
import Stripe from "stripe";
async function hmacSHA256Hex(key: string, message: string): Promise<string> {
const enc = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
"raw",
enc.encode(key),
{ name: "HMAC", hash: { name: "SHA-256" } },
false,
["sign"],
);
const sig = await crypto.subtle.sign("HMAC", cryptoKey, enc.encode(message));
const bytes = new Uint8Array(sig);
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
}
function parseStripeSignature(header: string | null) {
if (!header) return null;
const parts = header.split(",").map((p) => p.trim());
const out: Record<string, string> = {};
for (const part of parts) {
const [k, v] = part.split("=");
if (k && v) out[k] = v;
}
if (!out["t"] || !out["v1"]) return null;
return { timestamp: out["t"], signature: out["v1"] };
}
export const handler = define.handlers({ export const handler = define.handlers({
async POST(ctx) { async POST(ctx) {
@ -72,23 +47,26 @@ export const handler = define.handlers({
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
} }
const parsed = parseStripeSignature(sigHeader);
if (!parsed) {
return new Response(JSON.stringify({ error: "Bad signature header" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const signedPayload = `${parsed.timestamp}.${raw}`;
const expected = await hmacSHA256Hex(secret, signedPayload);
if (expected !== parsed.signature) {
return new Response(JSON.stringify({ error: "Invalid signature" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const event = JSON.parse(raw || "{}"); let event;
try {
event = Stripe.webhooks.constructEvent(
raw,
sigHeader,
secret,
);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
return new Response(
JSON.stringify({
error: `Webhook verification failed: ${errorMessage}`,
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
if (event?.type === "payment_intent.succeeded") { if (event?.type === "payment_intent.succeeded") {
const obj = event.data?.object ?? {}; const obj = event.data?.object ?? {};
const stripeId = obj.id as string; const stripeId = obj.id as string;