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
|
||||
# Example: pk_test_51AbCdEfGhIjKlMnOpQrStUvWxYz1234567890
|
||||
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",
|
||||
"gLog": "npm:google-logging-utils",
|
||||
"cosmos": "npm:@azure/cosmos",
|
||||
"@playwright/test": "npm:@playwright/test"
|
||||
"@playwright/test": "npm:@playwright/test",
|
||||
"stripe": "npm:stripe@^17.3.0"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
|
||||
@ -1,12 +1,27 @@
|
||||
// routes/api/delete/lease_document.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 {
|
||||
getLeasesByOwner,
|
||||
getLeasesByTenant,
|
||||
} from "../../../services/leaseService.ts";
|
||||
|
||||
export const handler = define.handlers({
|
||||
async POST(ctx) {
|
||||
try {
|
||||
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") || "";
|
||||
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);
|
||||
|
||||
// Return appropriate response based on request type
|
||||
|
||||
@ -1,12 +1,24 @@
|
||||
// routes/api/delete/property_media.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 { getPropertiesByOwner } from "../../../services/propertyService.ts";
|
||||
|
||||
export const handler = define.handlers({
|
||||
async POST(ctx) {
|
||||
try {
|
||||
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") || "";
|
||||
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);
|
||||
|
||||
// Return appropriate response based on request type
|
||||
|
||||
@ -1,10 +1,21 @@
|
||||
// routes/api/delete/users.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";
|
||||
|
||||
export const handler = define.handlers({
|
||||
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 user_id = formData.get("user_id");
|
||||
const sender = formData.get("sender");
|
||||
@ -13,8 +24,37 @@ export const handler = define.handlers({
|
||||
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
|
||||
await deleteUser(user_id.toString());
|
||||
await deleteUser(targetUserId);
|
||||
|
||||
// Redirect back to the sender URL (must be absolute)
|
||||
const senderPath = sender.toString();
|
||||
|
||||
@ -1,10 +1,25 @@
|
||||
// routes/api/delete/user_roles.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";
|
||||
|
||||
export const handler = define.handlers({
|
||||
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 user_id = formData.get("user_id");
|
||||
const sender = formData.get("sender");
|
||||
@ -13,7 +28,36 @@ export const handler = define.handlers({
|
||||
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)
|
||||
const senderPath = sender.toString();
|
||||
|
||||
@ -3,32 +3,7 @@ import {
|
||||
createPaymentTransaction,
|
||||
getTransactionByStripeId,
|
||||
} from "../../../services/paymentService.ts";
|
||||
|
||||
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"] };
|
||||
}
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const handler = define.handlers({
|
||||
async POST(ctx) {
|
||||
@ -72,23 +47,26 @@ export const handler = define.handlers({
|
||||
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") {
|
||||
const obj = event.data?.object ?? {};
|
||||
const stripeId = obj.id as string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user