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:
parent
1df84f59a4
commit
744115d230
45
.env.example
45
.env.example
@ -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
3
deno.json
Executable file → Normal 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": [
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user