Ramadan Last 10 Nights Campaign

Used by thousands of students worldwide - help us reach 150 members

... / 150 members...
APIUsers & Authentication

Users & Authentication

/user

Account creation, email/password login, Google sign-in (browser redirect flow and mobile ID-token flow), password reset, and the current user's profile and profile picture. Successful logins return a JWT that must be sent as `Authorization: Bearer <token>` on authenticated requests.

POST/user/signup

Create an account with username, email and password

Public

Creates a new local account. The request is rejected if another user already has the same username or the same email. A Stripe customer is created for the new user and its id is stored as `stripeCustomerId`. New users receive the default `member` role. If `referralCode` matches an active affiliate code, a referral record is created for the new user (a failure to record the referral never blocks signup). A welcome email is sent asynchronously after the response. The response contains a JWT for immediate use.

Body parameters

usernamerequired
string
Desired username. Must be unique.
emailrequired
string
Email address. Must be unique.
passwordrequired
string
Plain-text password; it is hashed with bcrypt before being stored.
referralCode
string
Affiliate referral code. Ignored if it does not match an active affiliate.
turnstileTokenrequired
string
Cloudflare Turnstile token from the challenge widget on the Sharh signup page. Tokens are single-use and expire after 5 minutes. Email sign-up is CAPTCHA-protected, so it cannot be scripted; Google sign-in is not challenged.

Errors

  • 400JSON `{"error":"CAPTCHA_REQUIRED","message":"...","signupUrl":"..."}` when turnstileToken is missing; plain text `Bad Request: Missing required fields` when username, password or email is missing; or `Bad Request: User with the same username or email already exists`.
  • 403JSON `{"error":"CAPTCHA_FAILED","message":"..."}` — the Turnstile token was invalid, expired or already used.
  • 503JSON `{"error":"CAPTCHA_UNAVAILABLE","message":"..."}` — the token could not be verified with Cloudflare; no account is created. Retry shortly.
  • 500Plain text `Internal Server Error` (database or Stripe customer creation failure).
Request
curl -X POST "https://app.ummahspot.com/user/signup" \
  -H "Content-Type: application/json" \
  -d '{"username":"string","email":"string","password":"string","turnstileToken":"string"}'
Response · 201
{
  "token": "eyJhbGciOi...",
  "user": {
    "id": "665f1c2e9b1d4a0012ab34cd",
    "username": "ibnrushd",
    "email": "ibnrushd@example.com",
    "stripeCustomerId": "cus_XXXXXXXXXXXXXX",
    "roles": ["member"],
    "profilePicture": null
  }
}
POST/user/login

Log in with username or email and password

Public

Looks the user up by `identifier`, which is matched against both the username and the email fields, then verifies the password. On success returns a JWT plus a summary of the user. `profilePicture` is a presigned S3 URL (valid for 1 hour) when the user has uploaded a picture, otherwise the picture URL from their Google account, otherwise `null`.

Body parameters

identifierrequired
string
The account's username or email address.
passwordrequired
string
The account password.

Errors

  • 400Plain text `Bad Request: Missing required fields` when identifier or password is missing.
  • 401Plain text `Unauthorized: Incorrect credentials` when no user matches the identifier or the password is wrong.
  • 500Plain text `Internal Server Error`.
Request
curl -X POST "https://app.ummahspot.com/user/login" \
  -H "Content-Type: application/json" \
  -d '{"identifier":"string","password":"string"}'
Response · 200
{
  "token": "eyJhbGciOi...",
  "user": {
    "id": "665f1c2e9b1d4a0012ab34cd",
    "username": "ibnrushd",
    "email": "ibnrushd@example.com",
    "stripeCustomerId": "cus_XXXXXXXXXXXXXX",
    "roles": ["member"],
    "profilePicture": "https://<bucket>.s3.amazonaws.com/profile-pictures/665f1c2e9b1d4a0012ab34cd-<uuid>.jpg?X-Amz-Signature=..."
  }
}
GET/user

Get the current user's profile

Auth required

Returns the full user document of the authenticated user with the `password` field removed. If the user has an uploaded profile picture (`profilePictureKey`), `profilePicture` is replaced with a presigned S3 URL valid for 1 hour; otherwise it keeps the URL from the Google account, if any.

Errors

  • 401Plain text `Unauthorized: No token provided`.
  • 403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.
  • 404Plain text `User not found`.
  • 500Plain text `Internal Server Error`.
Request
curl -X GET "https://app.ummahspot.com/user" \
  -H "Authorization: Bearer $SHARH_TOKEN"
Response · 200
{
  "_id": "665f1c2e9b1d4a0012ab34cd",
  "username": "ibnrushd",
  "email": "ibnrushd@example.com",
  "stripeCustomerId": "cus_XXXXXXXXXXXXXX",
  "roles": ["member"],
  "createdAt": "2025-01-12T09:30:00.000Z",
  "authMethod": "local",
  "profilePictureKey": "profile-pictures/665f1c2e9b1d4a0012ab34cd-<uuid>.jpg",
  "profilePicture": "https://<bucket>.s3.amazonaws.com/profile-pictures/665f1c2e9b1d4a0012ab34cd-<uuid>.jpg?X-Amz-Signature=...",
  "mobileSubscription": {
    "platform": null,
    "productId": "",
    "transactionId": "",
    "originalTransactionId": "",
    "status": "none",
    "isActive": false,
    "expiresAt": null,
    "environment": "",
    "lastValidatedAt": null,
    "source": ""
  },
  "followersCount": 0,
  "followingCount": 0,
  "creatorProfile": {
    "status": "none",
    "tagline": "",
    "bio": "",
    "publicationTitle": "",
    "slug": "",
    "coverImage": "",
    "bannerText": "",
    "featuredQuote": "",
    "socialLinks": {
      "website": "",
      "x": "",
      "youtube": "",
      "instagram": ""
    },
    "approvedAt": null,
    "approvedBy": null,
    "customPlatformFeePercent": null,
    "stripeConnectAccountId": "",
    "stripeConnectOnboarded": false,
    "stripeChargesEnabled": false,
    "stripePayoutsEnabled": false
  },
  "__v": 0
}
PUT/user/profile-picture

Upload or replace the current user's profile picture

Auth required

Uploads an image to S3 under `profile-pictures/` and stores its key on the user. If the user already had an uploaded picture, the old S3 object is deleted. The response contains a presigned URL for the new picture, valid for 1 hour. Only JPEG, PNG, GIF and WebP images up to 5 MB are accepted.

Send the body as multipart/form-data.

Body parameters

profilePicturerequired
file
The image file. Allowed MIME types: image/jpeg, image/png, image/gif, image/webp. Maximum size 5 MB.

Errors

  • 400JSON `{ "error": "No image file provided" }` when the `profilePicture` field is missing.
  • 401Plain text `Unauthorized: No token provided`.
  • 403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.
  • 404User not found.
  • 500JSON `{ "error": "Failed to upload profile picture" }` on S3/database failure. A file with a disallowed type or larger than 5 MB is rejected by the upload middleware and surfaces as a plain-text 500 from the global error handler.
Request
curl -X PUT "https://app.ummahspot.com/user/profile-picture" \
  -H "Authorization: Bearer $SHARH_TOKEN" \
  -F "profilePicture=@/path/to/file"
Response · 200
{
  "profilePicture": "https://<bucket>.s3.amazonaws.com/profile-pictures/665f1c2e9b1d4a0012ab34cd-<uuid>.png?X-Amz-Signature=..."
}
DELETE/user/profile-picture

Remove the current user's uploaded profile picture

Auth required

Deletes the uploaded picture from S3 (if there is one) and clears it from the user. The response returns the picture the account falls back to: the Google account picture URL if the user has one, otherwise `null`.

Errors

  • 401Plain text `Unauthorized: No token provided`.
  • 403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.
  • 404User not found.
  • 500JSON `{ "error": "Failed to delete profile picture" }`.
Request
curl -X DELETE "https://app.ummahspot.com/user/profile-picture" \
  -H "Authorization: Bearer $SHARH_TOKEN"
Response · 200
{
  "profilePicture": null
}
POST/user/forgot-password

Request a password reset email

Public

Looks up the account by the lower-cased email. If one exists, a reset token valid for 1 hour is generated, stored on the user, and emailed as a link of the form `https://sharhapp.com/reset-password?token=<token>`. The response is identical whether or not an account exists, so the endpoint does not reveal which emails are registered.

Body parameters

emailrequired
string
Email address of the account. Must contain an `@`.

Errors

  • 400JSON `{ "error": "Valid email address is required" }` when email is missing or has no `@`.
  • 500JSON `{ "error": "Internal Server Error" }`.
Request
curl -X POST "https://app.ummahspot.com/user/forgot-password" \
  -H "Content-Type: application/json" \
  -d '{"email":"string"}'
Response · 200
{
  "message": "If an account with that email exists, a password reset link has been sent"
}
POST/user/reset-password

Set a new password using a reset token

Public

Verifies the reset token from the password reset email. The token must be a valid, unexpired password-reset token, must match the token currently stored on the user, and the stored expiry must not have passed. On success the password is replaced and the stored token is cleared, so each token can be used only once.

Body parameters

tokenrequired
string
The `token` query parameter from the reset link sent by email.
newPasswordrequired
string
The new password.

Errors

  • 400JSON `{ "error": "..." }` with one of: `Token and new password are required`, `Invalid or expired token`, `Invalid token type`, `Invalid token or user not found`, `Token expired or invalid`.
  • 500JSON `{ "error": "Internal Server Error" }`.
Request
curl -X POST "https://app.ummahspot.com/user/reset-password" \
  -H "Content-Type: application/json" \
  -d '{"token":"string","newPassword":"string"}'
Response · 200
{
  "message": "Password reset successful"
}
GET/user/auth/google

Start the Google OAuth sign-in redirect flow

Public

Browser-navigation endpoint, not a JSON API. It packs the query parameters below into a base64url-encoded JSON `state` value and redirects the browser to Google's consent screen requesting the `profile` and `email` scopes. After the user approves, Google redirects the browser to `GET /user/auth/google/callback`, which finishes the sign-in and redirects back to the frontend (or to `redirectUrl` for the extension flow) with the token. Because the callback hands the session token to the redirect target, both redirect parameters are validated: `redirect` must be a path on the Sharh site, and `redirectUrl` is only accepted when it is a Chrome extension identity URL (`https://<32-character-extension-id>.chromiumapp.org/...`, as returned by `chrome.identity.getRedirectURL()`); any other `redirectUrl` is rejected with a 400 before the redirect to Google.

Query parameters

redirect
string
Same-site frontend path to send the user to after sign-in. It must start with a single "/" — a value that does not start with "/", starts with "//", or contains a backslash (for example an absolute URL to another origin) is replaced with "/". It is passed back as the `redirect` query parameter of the final frontend redirect.Default: /
source
string
Where the flow started. `extension` (together with `redirectUrl`) makes the callback redirect to `redirectUrl` instead of the web frontend; any other value uses the web flow.Default: web
redirectUrl
string
URL the callback redirects to when `source` is `extension`. Must be an `https://<extension-id>.chromiumapp.org/...` URL (the value of `chrome.identity.getRedirectURL()`): https only, the host is a 32-character extension id (letters a-p) followed by `.chromiumapp.org`, with no port or credentials. When the server is configured with an ALLOWED_EXTENSION_IDS list, the extension id must also be on that list. Anything else results in a 400.
ref
string
Affiliate referral code. Recorded only if the sign-in creates a new account and the code matches an active affiliate.

Errors

  • 400`redirectUrl` was supplied but is not an allowed `https://<extension-id>.chromiumapp.org/...` URL (JSON: `{ "message": "Invalid redirectUrl" }`)
Request
curl -X GET "https://app.ummahspot.com/user/auth/google"
Response · 302
Redirects (302) to the Google OAuth consent screen. No JSON body.
GET/user/auth/google/callback

Google OAuth callback that completes sign-in

Public

Called by Google, via browser redirect, after the consent screen; clients do not call it directly. The Google profile is resolved to a Sharh user in this order: an existing user with the same Google id; otherwise an existing user with the same email, whose account is then linked to Google (`authMethod` becomes `both` for a local account); otherwise a new user is created with a username derived from the email local part (a numeric suffix is added if taken), a new Stripe customer and the `member` role. A JWT is issued and the `state` value is decoded to recover `redirectTo`, `source`, `redirectUrl` and the referral code. Because the state round-trips through the browser unsigned, its values are re-validated exactly as in `GET /user/auth/google`: a `redirectTo` that is not a same-site path (starting with a single "/") is replaced with "/", and a `redirectUrl` that is not an allowed `https://<extension-id>.chromiumapp.org/...` URL is discarded. If the user was newly created and the referral code matches an active affiliate, a referral is recorded. The browser is then redirected. If `source` is `extension` and a valid `redirectUrl` was supplied, the redirect goes to `<redirectUrl>?token=<jwt>&user=<url-encoded JSON>`. Otherwise — including when the `redirectUrl` in the state was invalid — the normal web flow is used and it goes to `<FRONTEND_URL>/auth/callback?token=<jwt>&user=<url-encoded JSON>&redirect=<url-encoded redirect path>`, where the frontend base URL defaults to `https://sharhapp.com`. The `user` parameter is a URL-encoded JSON object with the fields `id`, `username`, `email`, `stripeCustomerId`, `roles`, `authMethod` and `profilePicture` (a presigned S3 URL if the user uploaded a picture, else the Google picture URL). If Google authentication fails, the browser is redirected to `<FRONTEND_URL>/login?error=oauth_failed`. If an error occurs while completing sign-in, it is redirected to `<FRONTEND_URL>/login?error=auth_error`.

Query parameters

coderequired
string
Authorization code appended by Google; consumed by the OAuth middleware.
state
string
The base64url-encoded JSON state created by `GET /user/auth/google`. When missing or undecodable, the defaults are used (`redirect` = `/`, `source` = `web`). Invalid `redirectTo` / `redirectUrl` values inside it are ignored.
Request
curl -X GET "https://app.ummahspot.com/user/auth/google/callback"
Response · 302
Redirects (302) to the frontend with the credentials in the query string, e.g.

https://sharhapp.com/auth/callback?token=eyJhbGciOi...&user=%7B%22id%22%3A%22665f1c2e9b1d4a0012ab34cd%22%2C...%7D&redirect=%2Fbooks

Decoded "user" parameter:
{
  "id": "665f1c2e9b1d4a0012ab34cd",
  "username": "ibnrushd",
  "email": "ibnrushd@example.com",
  "stripeCustomerId": "cus_XXXXXXXXXXXXXX",
  "roles": ["member"],
  "authMethod": "google",
  "profilePicture": "https://lh3.googleusercontent.com/a/..."
}
POST/user/auth/google/unlink

Unlink the Google account from the current user

Auth required

Removes the Google id from the authenticated user and sets `authMethod` to `local`. Refused when the account was created through Google and has no password, since the user would be left with no way to log in; set a password first (for example through the password reset flow).

Errors

  • 400JSON `{ "error": "Cannot unlink Google account: No password set. Please set a password first." }`.
  • 401Plain text `Unauthorized: No token provided`.
  • 403Plain text `Forbidden: Invalid token`, or `Forbidden: You do not have the required permissions`.
  • 404Plain text `User not found`.
  • 500Plain text `Internal Server Error`.
Request
curl -X POST "https://app.ummahspot.com/user/auth/google/unlink" \
  -H "Authorization: Bearer $SHARH_TOKEN"
Response · 200
{
  "message": "Google account unlinked successfully",
  "user": {
    "id": "665f1c2e9b1d4a0012ab34cd",
    "username": "ibnrushd",
    "email": "ibnrushd@example.com",
    "authMethod": "local"
  }
}
POST/user/auth/google/mobile

Sign in with a Google ID token (mobile)

Public

For native apps that obtain a Google ID token from the Google Sign-In SDK. The token is verified against the server's Google client id, then the Google profile is resolved to a Sharh user exactly as in the browser flow: match by Google id, else match by email and link the account, else create a new user (with a Stripe customer and the `member` role). Returns a JWT and a summary of the user. `profilePicture` is a presigned S3 URL (valid 1 hour) if the user uploaded a picture, otherwise the Google picture URL.

Body parameters

idTokenrequired
string
Google ID token issued for the app's Google client id.

Errors

  • 400JSON `{ "message": "idToken is required" }` or `{ "message": "Google account has no email" }`.
  • 401JSON `{ "message": "Google token expired. Please try again." }` when the ID token is too old, or `{ "message": "Invalid Google token" }` for any other verification or sign-in failure.
Request
curl -X POST "https://app.ummahspot.com/user/auth/google/mobile" \
  -H "Content-Type: application/json" \
  -d '{"idToken":"string"}'
Response · 200
{
  "token": "eyJhbGciOi...",
  "user": {
    "id": "665f1c2e9b1d4a0012ab34cd",
    "username": "ibnrushd",
    "email": "ibnrushd@example.com",
    "stripeCustomerId": "cus_XXXXXXXXXXXXXX",
    "roles": ["member"],
    "authMethod": "google",
    "profilePicture": "https://lh3.googleusercontent.com/a/..."
  }
}