code:
  Next refresh: <time><%- nextQuotaRefresh %></time>

<%
const quotaTableId = Math.random().toString(36).slice(2);
 %>

  <label for="quota-family-filter-<%= quotaTableId %>">Filter:
  <input type="text" id="quota-family-filter-<%= quotaTableId %>" oninput="filterQuotaTable(this, '<%= quotaTableId %>')" />

<table class="striped" id="quota-table-<%= quotaTableId %>">

      Model Family
      Usage
      <% if (showTokenCosts) { %>
      Cost
      <% } %>
      Limit
      Remaining
      <th scope="col" colspan="<%= showRefreshEdit ? 2 : 1 %>">Refresh Amount

    <% Object.entries(quota).forEach(([key, limit]) => { %>

      <%- key %>
      <%- prettyTokens(user.tokenCounts[key]) %>
      <% if (showTokenCosts) { %>
      $<%- tokenCost(key, user.tokenCounts[key]).toFixed(2) %>
      <% } %>
      <% if (!user.tokenLimits[key]) { %>
      unlimited
      <% } else { %>
      <%- prettyTokens(user.tokenLimits[key]) %>
      <%- prettyTokens(user.tokenLimits[key] - user.tokenCounts[key]) %>
      <% } %>
      <% if (user.type === "temporary") { %>
      N/A
      <% } else { %>
      <%- prettyTokens(user.tokenRefresh[key] || quota[key]) %>
      <% } %>
      <% if (showRefreshEdit) { %>

        <a
          title="Edit"
          id="edit-refresh"
          href="#"
          data-field="tokenRefresh_<%= key %>"
          data-token="<%= user.token %>"
          data-modelFamily="<%= key %>"
          >✏

      <% } %>

    <% }) %>


code:
Show all (<%- user.ip.length %>)

  <% user.ip.forEach((ip) => { %>

<%- shouldRedact ? redactIp(ip) : ip %>

  <% }) %>

license: unknown


code:
import cookieParser from "cookie-parser";
import expressSession from "express-session";
import MemoryStore from "memorystore";
import { config, SECRET_SIGNING_KEY } from "../config";

const ONE_WEEK = 1000 * 60 * 60 * 24 * 7;

const cookieParserMiddleware = cookieParser(SECRET_SIGNING_KEY);

const sessionMiddleware = expressSession({
  secret: SECRET_SIGNING_KEY,
  resave: false,
  saveUninitialized: false,
  store: new (MemoryStore(expressSession))({ checkPeriod: ONE_WEEK }),
  cookie: {
    sameSite: "strict",
    maxAge: ONE_WEEK,
    signed: true,
    secure: !config.useInsecureCookies,
  },
});

const withSession = [cookieParserMiddleware, sessionMiddleware];

export { withSession };


code:
import express, { Router } from "express";
import { injectCsrfToken, checkCsrfToken } from "../shared/inject-csrf";
import { browseImagesRouter } from "./web/browse-images";
import { selfServiceRouter } from "./web/self-service";
import { powRouter } from "./web/pow-captcha";
import { injectLocals } from "../shared/inject-locals";
import { withSession } from "../shared/with-session";
import { config } from "../config";

const userRouter = Router();

userRouter.use(
  express.json({ limit: "1mb" }),
  express.urlencoded({ extended: true, limit: "1mb" })
);
userRouter.use(withSession);
userRouter.use(injectCsrfToken, checkCsrfToken);
userRouter.use(injectLocals);
if (config.showRecentImages) {
  userRouter.use(browseImagesRouter);
}
if (config.captchaMode !== "none") {
  userRouter.use("/captcha", powRouter);
}
userRouter.use(selfServiceRouter);

userRouter.use(
  (
    err: Error,
    req: express.Request,
    res: express.Response,
    _next: express.NextFunction
  ) => {
    const data: any = { message: err.message, stack: err.stack, status: 500 };
    const isCsrfError = err.message === "invalid csrf token";

    if (isCsrfError) {
      res.clearCookie("csrf");
      req.session.csrf = undefined;
    }

    if (req.accepts("json", "html") === "json") {
      const message = isCsrfError
        ? "CSRF token mismatch; try refreshing the page"
        : err.message;

      return res.status(500).json({ error: message });
    } else {
      return res.status(500).render("user_error", { ...data, flash: null });
    }
  }
);

export { userRouter };


code:
import express, { Request, Response } from "express";
import { getLastNImages } from "../../shared/file-storage/image-history";
import { paginate } from "../../shared/utils";
import { ipLimiter } from "../../proxy/rate-limit";

const IMAGES_PER_PAGE = 24;

const metadataCacheTTL = 1000 * 60 * 3;
let metadataCache: string | null = null;
let metadataCacheValid = 0;

const handleImageHistoryPage = (req: Request, res: Response) => {
  const page = parseInt(req.query.page as string) || 1;
  const allImages = getLastNImages();
  const { items, pageCount } = paginate(allImages, page, IMAGES_PER_PAGE);

  res.render("image_history", {
    images: items,
    pagination: {
      currentPage: page,
      totalPages: pageCount,
    },
  });
};

const handleMetadataRequest = (_req: Request, res: Response) => {
  res.setHeader("Cache-Control", "public, max-age=180");
  res.setHeader("Content-Type", "application/json");
  res.setHeader(
    "Content-Disposition",
    `attachment; filename="image-metadata-${new Date().toISOString()}.json"`
  );
  if (new Date().getTime() - metadataCacheValid < metadataCacheTTL) {
    return res.status(200).send(metadataCache);
  }

  const images = getLastNImages().map(({ prompt, url }) => ({ url, prompt }));
  const metadata = {
    exportedAt: new Date().toISOString(),
    totalImages: images.length,
    images,
  };
  metadataCache = JSON.stringify(metadata, null, 2);
  metadataCacheValid = new Date().getTime();
  res.status(200).send(metadataCache);
};

export const browseImagesRouter = express.Router();
browseImagesRouter.get("/image-history", handleImageHistoryPage);
browseImagesRouter.get(
  "/image-history/metadata",
  ipLimiter,
  handleMetadataRequest
);


code:
import crypto from "crypto";
import express from "express";
import argon2 from "@node-rs/argon2";
import { z } from "zod";
import { signMessage } from "../../shared/hmac-signing";
import {
  authenticate,
  createUser,
  getUser,
  upsertUser,
} from "../../shared/users/user-store";
import { config } from "../../config";

/** Lockout time after verification in milliseconds */
const LOCKOUT_TIME = 1000 * 60; // 60 seconds

let powKeySalt = crypto.randomBytes(32).toString("hex");

/**
 * Invalidates any outstanding unsolved challenges.
 */
export function invalidatePowChallenges() {
  powKeySalt = crypto.randomBytes(32).toString("hex");
}

const argon2Params = {
  ARGON2_TIME_COST: parseInt(process.env.ARGON2_TIME_COST || "8"),
  ARGON2_MEMORY_KB: parseInt(process.env.ARGON2_MEMORY_KB || String(1024 * 64)),
  ARGON2_PARALLELISM: parseInt(process.env.ARGON2_PARALLELISM || "1"),
  ARGON2_HASH_LENGTH: parseInt(process.env.ARGON2_HASH_LENGTH || "32"),
};

/**
 * Work factor for each difficulty. This is the expected number of hashes that
 * will be computed to solve the challenge, on average. The actual number of
 * hashes will vary due to randomness.
 */
const workFactors = { extreme: 4000, high: 1900, medium: 900, low: 200 };

type Challenge = {
  /** Salt */
  s: string;
  /** Argon2 hash length */
  hl: number;
  /** Argon2 time cost */
  t: number;
  /** Argon2 memory cost */
  m: number;
  /** Argon2 parallelism */
  p: number;
  /** Challenge target value (difficulty) */
  d: string;
  /** Expiry time in milliseconds */
  e: number;
  /** IP address of the client */
  ip?: string;
  /** Challenge version */
  v?: number;
  /** Usertoken for refreshing */
  token?: string;
};

const verifySchema = z.object({
  challenge: z.object({
    s: z
      .string()
      .min(1)
      .max(64)
      .regex(/^[0-9a-f]+$/),
    hl: z.number().int().positive().max(64),
    t: z.number().int().positive().min(2).max(10),
    m: z
      .number()
      .int()
      .positive()
      .max(1024 * 1024 * 2),
    p: z.number().int().positive().max(16),
    d: z.string().regex(/^[0-9]+n$/),
    e: z.number().int().positive(),
    ip: z.string().min(1).max(64).optional(),
    v: z.literal(1).optional(),
    token: z.string().min(1).max(64).optional(),
  }),
  solution: z.string().min(1).max(64),
  signature: z.string().min(1),
  proxyKey: z.string().min(1).max(1024).optional(),
});

const challengeSchema = z.object({
  action: z.union([z.literal("new"), z.literal("refresh")]),
  refreshToken: z.string().min(1).max(64).optional(),
  proxyKey: z.string().min(1).max(1024).optional(),
});

/** Solutions by timestamp */
const solves = new Map<string, number>();
/** Recent attempts by IP address */
const recentAttempts = new Map<string, number>();

setInterval(() => {
  const now = Date.now();
  for (const [ip, timestamp] of recentAttempts) {
    if (now - timestamp > LOCKOUT_TIME) {
      recentAttempts.delete(ip);
    }
  }

  for (const [key, timestamp] of solves) {
    if (now - timestamp > config.powChallengeTimeout * 1000 * 60) {
      solves.delete(key);
    }
  }
}, 1000);

function generateChallenge(clientIp?: string, token?: string): Challenge {
  let workFactor =
    (typeof config.powDifficultyLevel === "number"
      ? config.powDifficultyLevel
      : workFactors[config.powDifficultyLevel]) || 1000;

  // If this is a token refresh, halve the work factor
  if (token) {
    workFactor = Math.floor(workFactor / 2);
  }

  const hashBits = BigInt(argon2Params.ARGON2_HASH_LENGTH) * 8n;
  const hashMax = 2n ** hashBits;
  const targetValue = hashMax / BigInt(workFactor);

  return {
    s: crypto.randomBytes(32).toString("hex"),
    hl: argon2Params.ARGON2_HASH_LENGTH,
    t: argon2Params.ARGON2_TIME_COST,
    m: argon2Params.ARGON2_MEMORY_KB,
    p: argon2Params.ARGON2_PARALLELISM,
    d: targetValue.toString() + "n",
    e: Date.now() + config.powChallengeTimeout * 1000 * 60,
    ip: clientIp,
    token,
  };
}

async function verifySolution(
  challenge: Challenge,
  solution: string,
  logger: any
): Promise<boolean> {
  logger.info({ solution, challenge }, "Verifying solution");
  const hash = await argon2.hashRaw(String(solution), {
    salt: Buffer.from(challenge.s, "hex"),
    outputLen: challenge.hl,
    timeCost: challenge.t,
    memoryCost: challenge.m,
    parallelism: challenge.p,
    algorithm: argon2.Algorithm.Argon2id,
  });
  const hashStr = hash.toString("hex");
  const target = BigInt(challenge.d.slice(0, -1));
  const hashValue = BigInt("0x" + hashStr);
  const result = hashValue <= target;
  logger.info({ hashStr, target, hashValue, result }, "Solution verified");
  return result;
}

function verifyTokenRefreshable(token: string, req: express.Request) {
  const ip = req.ip;

  const user = getUser(token);
  if (!user) {
    req.log.warn({ token }, "Cannot refresh token - not found");
    return false;
  }
  if (user.type !== "temporary") {
    req.log.warn({ token }, "Cannot refresh token - wrong token type");
    return false;
  }
  if (!user.meta?.refreshable) {
    req.log.warn({ token }, "Cannot refresh token - not refreshable");
    return false;
  }
  if (!user.ip.includes(ip)) {
    // If there are available slots, add the IP to the list
    const { result } = authenticate(token, ip);
    if (result === "limited") {
      req.log.warn({ token, ip }, "Cannot refresh token - IP limit reached");
      return false;
    }
  }

  req.log.info({ token: `...${token.slice(-5)}` }, "Allowing token refresh");
  return true;
}

const router = express.Router();
router.post("/challenge", (req, res) => {
  const data = challengeSchema.safeParse(req.body);
  if (!data.success) {
    res
      .status(400)
      .json({ error: "Invalid challenge request", details: data.error });
    return;
  }
  const { action, refreshToken, proxyKey } = data.data;
  if (config.proxyKey && proxyKey !== config.proxyKey) {
    res.status(401).json({ error: "Invalid proxy password" });
    return;
  }

  if (action === "refresh") {
    if (!refreshToken || !verifyTokenRefreshable(refreshToken, req)) {
      res.status(400).json({
        error: "Not allowed to refresh that token; request a new one",
      });
      return;
    }
    const challenge = generateChallenge(req.ip, refreshToken);
    const signature = signMessage(challenge, powKeySalt);
    res.json({ challenge, signature });
  } else {
    const challenge = generateChallenge(req.ip);
    const signature = signMessage(challenge, powKeySalt);
    res.json({ challenge, signature });
  }
});

router.post("/verify", async (req, res) => {
  const ip = req.ip;
  req.log.info("Got verification request");
  if (recentAttempts.has(ip)) {
    const error = "Rate limited; wait a minute before trying again";
    req.log.info({ error }, "Verification rejected");
    res.status(429).json({ error });
    return;
  }

  const result = verifySchema.safeParse(req.body);
  if (!result.success) {
    const error = "Invalid verify request";
    req.log.info({ error, result }, "Verification rejected");
    res.status(400).json({ error, details: result.error });
    return;
  }

  const { challenge, signature, solution } = result.data;
  if (signMessage(challenge, powKeySalt) !== signature) {
    const error =
      "Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.";
    req.log.info({ error }, "Verification rejected");
    res.status(400).json({ error });
    return;
  }

  if (config.proxyKey && result.data.proxyKey !== config.proxyKey) {
    const error = "Invalid proxy password";
    req.log.info({ error }, "Verification rejected");
    res.status(401).json({ error, password: result.data.proxyKey });
    return;
  }

  if (challenge.ip && challenge.ip !== ip) {
    const error = "Solution must be verified from original IP address";
    req.log.info(
      { error, challengeIp: challenge.ip, clientIp: ip },
      "Verification rejected"
    );
    res.status(400).json({ error });
    return;
  }

  if (solves.has(signature)) {
    const error = "Reused signature";
    req.log.info({ error }, "Verification rejected");
    res.status(400).json({ error });
    return;
  }

  if (Date.now() > challenge.e) {
    const error = "Verification took too long";
    req.log.info({ error }, "Verification rejected");
    res.status(400).json({ error });
    return;
  }

  if (challenge.token && !verifyTokenRefreshable(challenge.token, req)) {
    res.status(400).json({ error: "Not allowed to refresh that usertoken" });
    return;
  }

  recentAttempts.set(ip, Date.now());
  try {
    const success = await verifySolution(challenge, solution, req.log);
    if (!success) {
      recentAttempts.set(ip, Date.now() + 1000 * 60 * 60 * 6);
      req.log.warn("Bogus solution, client blocked");
      res.status(400).json({ error: "Solution failed verification" });
      return;
    }
    solves.set(signature, Date.now());
  } catch (err) {
    req.log.error(err, "Error verifying proof-of-work");
    res.status(500).json({ error: "Internal error" });
    return;
  }

  if (challenge.token) {
    const user = getUser(challenge.token);
    if (user) {
      upsertUser({
        token: challenge.token,
        expiresAt: Date.now() + config.powTokenHours * 60 * 60 * 1000,
        disabledAt: null,
        disabledReason: null,
      });
      req.log.info(
        { token: `...${challenge.token.slice(-5)}` },
        "Token refreshed"
      );
      return res.json({ success: true, token: challenge.token });
    }
  } else {
    const newToken = issueToken(req);
    return res.json({ success: true, token: newToken });
  }
});

router.get("/", (_req, res) => {
  res.render("user_request_token", {
    keyRequired: !!config.proxyKey,
    difficultyLevel: config.powDifficultyLevel,
    tokenLifetime: config.powTokenHours,
    tokenMaxIps: config.powTokenMaxIps,
    challengeTimeout: config.powChallengeTimeout,
  });
});

// const ipTokenCache = new Map<string, Set<string>>();
//
// function buildIpTokenCountCache() {
//   ipTokenCache.clear();
//   const users = getUsers().filter((u) => u.type === "temporary");
//   for (const user of users) {
//     for (const ip of user.ip) {
//       if (!ipTokenCache.has(ip)) {
//         ipTokenCache.set(ip, new Set());
//       }
//       ipTokenCache.get(ip)?.add(user.token);
//     }
//   }
// }

function issueToken(req: express.Request) {
  const token = createUser({
    type: "temporary",
    expiresAt: Date.now() + config.powTokenHours * 60 * 60 * 1000,
  });
  upsertUser({
    token,
    ip: [req.ip],
    maxIps: config.powTokenMaxIps,
    meta: { refreshable: true },
  });
  req.log.info(
    { ip: req.ip, token: `...${token.slice(-5)}` },
    "Proof-of-work token issued"
  );
  return token;
}

export { router as powRouter };


code:
import { Router } from "express";
import { UserPartialSchema } from "../../shared/users/schema";
import * as userStore from "../../shared/users/user-store";
import { ForbiddenError, BadRequestError } from "../../shared/errors";
import { sanitizeAndTrim } from "../../shared/utils";
import { config } from "../../config";

const router = Router();

router.use((req, res, next) => {
  if (req.session.userToken) {
    res.locals.currentSelfServiceUser =
      userStore.getUser(req.session.userToken) || null;
  }
  next();
});

router.get("/", (_req, res) => {
  res.redirect("/");
});

router.get("/lookup", (_req, res) => {
  const ipLimit =
    (res.locals.currentSelfServiceUser?.maxIps ?? config.maxIpsPerUser) || 0;
  res.render("user_lookup", {
    user: res.locals.currentSelfServiceUser,
    ipLimit,
  });
});

router.post("/lookup", (req, res) => {
  const token = req.body.token;
  const user = userStore.getUser(token);
  req.log.info(
    { token: truncateToken(token), success: !!user },
    "User self-service lookup"
  );
  if (!user) {
    req.session.flash = { type: "error", message: "Invalid user token." };
    return res.redirect("/user/lookup");
  }
  req.session.userToken = user.token;
  return res.redirect("/user/lookup");
});

router.post("/edit-nickname", (req, res) => {
  const existing = res.locals.currentSelfServiceUser;

  if (!existing) {
    throw new ForbiddenError("Not logged in.");
  } else if (!config.allowNicknameChanges || existing.disabledAt) {
    throw new ForbiddenError("Nickname changes are not allowed.");
  } else if (!config.maxIpsAutoBan && !existing.ip.includes(req.ip)) {
    throw new ForbiddenError(
      "Nickname changes are only allowed from registered IPs."
    );
  }

  const schema = UserPartialSchema.pick({ nickname: true })
    .strict()
    .transform((v) => ({ nickname: sanitizeAndTrim(v.nickname) }));

  const result = schema.safeParse(req.body);
  if (!result.success) {
    throw new BadRequestError(result.error.message);
  }

  const newNickname = result.data.nickname || null;
  userStore.upsertUser({ token: existing.token, nickname: newNickname });
  req.session.flash = { type: "success", message: "Nickname updated." };
  return res.redirect("/user/lookup");
});

function truncateToken(token: string) {
  const sliceLength = Math.max(Math.floor(token.length / 8), 1);
  return `${token.slice(0, sliceLength)}...${token.slice(-sliceLength)}`;
}

export { router as selfServiceRouter };


code:
<%- include("partials/shared_header", { title: "Image History" }) %>

Image History

<% if (images && images.length > 0) { %>

    <% images.forEach(function(image) { %>

        <% const thumbUrl = image.url.replace(/\.png$/, "_t.jpg"); %>
        <a href="<%= image.url %>" target="_blank">
          <img src="<%= thumbUrl %>" alt="<%= image.prompt %>" title="<%= image.prompt %>" />

    <% }); %>

Download JSON metadata for all images (data may be delayed)

  <% if (pagination && pagination.totalPages > 1) { %>

      <% const pageWindow = 5; %>
      <% const startPage = Math.max(1, pagination.currentPage - pageWindow); %>
      <% const endPage = Math.min(pagination.totalPages, pagination.currentPage + pageWindow); %>
      <% if (startPage > 1) { %>
        1
        <% if (startPage > 2) { %>...<% } %>
      <% } %>
      <% for (let i = startPage; i <= endPage; i++) { %>
        <a href="?page=<%= i %>" class="<%= i === pagination.currentPage ? 'active' : '' %>"><%= i %>
      <% } %>
      <% if (endPage < pagination.totalPages) { %>
        <% if (endPage < pagination.totalPages - 1) { %>...<% } %>
        <a href="?page=<%= pagination.totalPages %>"><%= pagination.totalPages %>
      <% } %>

  <% } %>
<% } else { %>

No images found.

<% } %>

<%- include("partials/user_footer") %>


code:
    JavaScript needs to be enabled to complete verification.

    Your device needs to be verified before you can receive a token. This might take anywhere from a few seconds to a
    few minutes, depending on your device and the proxy's security settings.

    What is this?

      This is a proof-of-work verification
      task designed to slow down automated abuse. It requires your device's CPU to find a solution to a cryptographic
      puzzle, after which a user token will be issued.

    How long does verification take?

      It depends on the device you're using and the current difficulty level (<%= difficultyLevel %>). The
      faster your device, the quicker it will solve the task.

      An estimate will be displayed once verification starts. Because the task is probabilistic, your device could solve
      it more quickly or take longer than the estimate.

    How often do I need to do this?

      Once you've earned a user token, you can use it for <%= `${tokenLifetime} hours` %> before it
      expires.

      You can refresh an expired token by returning to this page and verifying again. Subsequent verifications will go
      faster than the first one.

    Other important information

Don't change your IP address during verification.

Don't close this tab until verification is complete.

        Verification must be finished within <%= `${challengeTimeout} minutes` %>.

Your user token will be registered to your current IP address.

        Up to <%= tokenMaxIps || "unlimited" %> IP addresses total can be registered to your user
        token.

    Settings

      Workers:
      <input type="number" id="workers" value="1" min="1" max="32" onchange="spawnWorkers()" />

      This controls how many CPU cores will be used to solve the verification task. If your device gets too hot or slows
      down too much during verification, reduce the number of workers.

      For fastest verification, set this to the number of physical CPU cores in your device. Setting more workers than
      you have actual cores will generally only slow down verification.

If you don't understand what this means, leave it at the default setting.

  <form id="captcha-form" style="display: none">
    <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
    <input type="hidden" name="tokenLifetime" value="<%= tokenLifetime %>" />
  </form>

    <button id="worker-control" onclick="toggleWorker()">Start verification</button>

    Status:

    <textarea disabled id="captcha-progress-text"></textarea>


code:
<%- include("partials/shared_header", { title: "Error" }) %>

⚠ Error <%= status %>: <%= message %>

      <% if (message.includes('csrf')) { %>

ℹ Refresh the previous page and then try again. If the problem persists, clear cookies for this site.

      <% } %>

<%= stack %>

      Go Back


code:
<!DOCTYPE html>

  <meta charset="UTF-8">
  <meta name="robots" content="noindex" />
  <title><%= title %></title>

    <%= pageHeader %>

Service Info

<%= JSON.stringify(serviceInfo, null, 2) %>


code:
<%- include("partials/shared_header", { title: "User Token Lookup" }) %>

User Token Lookup

Provide your user token to check your usage and quota information.

<form action="/user/lookup" method="post">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
  User Token
  <input type="password" name="token" value="<%= user?.token %>" />
  <input type="submit" value="Lookup" />
</form>
<% if (user) { %>

<% if (user.type === "temporary" && Boolean(user.disabledAt)) { %>
<%- include("partials/shared_flash", { flashData: {
      type: "info",
      message: "This temporary user token has expired and is no longer usable. These records will be deleted soon.",
    } }) %>
<% } else if (user.disabledAt) { %>
<%- include("partials/shared_flash", { flashData: {
      type: "warning",
      message: "This user token has been disabled." + (user.disabledReason ? ` Reason: ${user.disabledReason}` : ""),
    } }) %>
<% } %>

      User Token
       <%= "..." + user.token.slice(-5) %>

      Nickname
      <%= user.nickname ?? "none" %>

        ✏

      Type
      <%= user.type %>

      Prompts
      <%= user.promptCount %>

      Created At
      <%= user.createdAt %>

      Last Used At
      <%= user.lastUsedAt || "never" %>

      IPs<%= ipLimit ? ` (max ${ipLimit})` : "" %>
      <%- include("partials/shared_user_ip_list", { user, shouldRedact: true }) %>

    <% if (user.type === "temporary") { %>

      Expires At
      <%= user.expiresAt %>

    <% } %>

Quota Information

<%- include("partials/shared_quota-info", { quota, user, showRefreshEdit: false }) %>

<form id="edit-nickname-form" style="display: none" action="/user/edit-nickname" method="post">
  <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
  <input type="hidden" name="nickname" value="<%= user.nickname %>" />
</form>

<% } %> <%- include("partials/user_footer") %>


code:
<%- include("partials/shared_header", { title: "Request User Token" }) %>

Request User Token

You can request a temporary user token to use this proxy. The token will be valid for <%= tokenLifetime %> hours.

<% if (keyRequired) { %>

You need to supply the proxy password to request or refresh a token.

    Proxy password:
    <input type="password" id="proxy-key" />

<% } %>

  <button id="request-token" onclick="requestChallenge('new')">Get a new token</button>
  <button id="refresh-token-toggle" onclick="switchSection('refresh')">Refresh an old token</button>

Existing token:

  « Back

      If you have an existing or expired token, enter it here to try to refresh it by completing a shorter verification.

      Existing token:
      <input type="text" id="refresh-token-input" />
      <button id="refresh-token" onclick="requestChallenge('refresh')">Refresh</button>

<%- include("partials/user_challenge_widget") %>

<%- include("partials/user_footer") %>


code:
# Ensure that the android contents are checked out as lf
devTools/androidsdk/image text eol=lf

repo_name: Area/degrees

path: .gitattributes

license: mit


code:
# IDE, temp, generated, and other local-only files
.*
*~
*.iml
node_modules/
tweego*
Degrees of Lewdity*.html
*.7z
*.zip
Zimg/
gitw
*.kra
Desktop.ini
Thumbs.db

# Ignoring working folder for Quin2k
mod/
*.code-workspace
release/

# Error logs
yarn-error.log

# Keys
/keys/*
!/keys/README.md

# Dist
/dist/*
# We use a placeholder to ensure the host system creates the folder
#  (may run into permissions issues if you allow docker to create it)
!/dist/.placeholder
/debug.log

path: .gitignore