FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

10 месяцев назад
​
1 год назад
9 месяцев назад
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
10 месяцев назад
​
​
​
​
​
​
​
10 месяцев назад
​
​
​
​
​
​
10 месяцев назад
​
​
​
​
​
​
​
​
​
​
10 месяцев назад
1 год назад
​
​
​
​
​
​
​
​
​
​
​
10 месяцев назад
​
​
1 год назад
​
​
​
​
​
10 месяцев назад
​
1 год назад
​
10 месяцев назад
​
​
​
​
1 год назад
10 месяцев назад
1 год назад
​
​
​
​
​
​
​
10 месяцев назад
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
10 месяцев назад
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
10 месяцев назад
1 год назад
10 месяцев назад
​
​
​
​
​
​
10 месяцев назад
​
​
​
1 год назад
10 месяцев назад
​
1 год назад
​
​
9 месяцев назад
​
​
​
​
9 месяцев назад
10 месяцев назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. // config/authConfig.ts (or wherever your authOptions live)
  2. import { AuthOptions } from "next-auth";
  3. import CredentialsProvider from "next-auth/providers/credentials";
  4. import { LOGIN_API_PATH, REFRESH_TOKEN_API_PATH } from "./api";
  5. import { Session } from "next-auth";
  6. import { JWT } from "next-auth/jwt";
  7. /** Refresh access token this many ms before backend JWT `exp`. */
  8. const ACCESS_TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1000;
  9. /** After a failed refresh, wait this long before trying again (avoids spam on session polls). */
  10. const REFRESH_FAILURE_COOLDOWN_MS = 60 * 1000;
  11. function backendAccessTokenExpiresAtMs(accessToken: string | null | undefined): number | null {
  12. if (!accessToken) return null;
  13. try {
  14. const parts = accessToken.split(".");
  15. if (parts.length < 2) return null;
  16. const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
  17. const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
  18. const json = Buffer.from(padded, "base64").toString("utf8");
  19. const payload = JSON.parse(json) as { exp?: number };
  20. return typeof payload.exp === "number" ? payload.exp * 1000 : null;
  21. } catch {
  22. return null;
  23. }
  24. }
  25. async function refreshAccessToken(token: JWT): Promise<JWT> {
  26. const refreshToken = token.refreshToken;
  27. if (!refreshToken || !REFRESH_TOKEN_API_PATH || REFRESH_TOKEN_API_PATH === "undefined/refresh-token") {
  28. return {
  29. ...token,
  30. error: "RefreshAccessTokenError",
  31. refreshFailedAt: Date.now(),
  32. };
  33. }
  34. try {
  35. const res = await fetch(REFRESH_TOKEN_API_PATH, {
  36. method: "POST",
  37. headers: { "Content-Type": "application/json" },
  38. body: JSON.stringify({ refreshToken }),
  39. });
  40. if (!res.ok) {
  41. return {
  42. ...token,
  43. error: "RefreshAccessTokenError",
  44. refreshFailedAt: Date.now(),
  45. };
  46. }
  47. const data = (await res.json()) as {
  48. accessToken?: string;
  49. refreshToken?: string;
  50. };
  51. if (!data.accessToken) {
  52. return {
  53. ...token,
  54. error: "RefreshAccessTokenError",
  55. refreshFailedAt: Date.now(),
  56. };
  57. }
  58. return {
  59. ...token,
  60. accessToken: data.accessToken,
  61. refreshToken: data.refreshToken ?? refreshToken,
  62. error: undefined,
  63. refreshFailedAt: undefined,
  64. };
  65. } catch {
  66. return {
  67. ...token,
  68. error: "RefreshAccessTokenError",
  69. refreshFailedAt: Date.now(),
  70. };
  71. }
  72. }
  73. // Extend the built-in types
  74. declare module "next-auth" {
  75. interface Session {
  76. accessToken: string | null;
  77. refreshToken?: string;
  78. abilities: string[];
  79. id?: string;
  80. /** JWT expiry (seconds since epoch); used to avoid redirecting to dashboard when token is expired */
  81. exp?: number;
  82. locale?: string;
  83. error?: string;
  84. }
  85. interface User {
  86. id?: string;
  87. accessToken: string | null;
  88. refreshToken?: string;
  89. abilities: string[];
  90. locale?: string;
  91. }
  92. }
  93. declare module "next-auth/jwt" {
  94. interface JWT {
  95. id?: string;
  96. accessToken: string | null;
  97. refreshToken?: string;
  98. abilities: string[];
  99. locale?: string;
  100. error?: string;
  101. /** Epoch ms of last failed refresh — used as cooldown to avoid hammering /refresh-token */
  102. refreshFailedAt?: number;
  103. }
  104. }
  105. export const authOptions: AuthOptions = {
  106. debug: process.env.NODE_ENV === "development",
  107. providers: [
  108. CredentialsProvider({
  109. id: "credentials",
  110. name: "Credentials",
  111. credentials: {
  112. username: { label: "Username", type: "text" },
  113. password: { label: "Password", type: "password" },
  114. },
  115. async authorize(credentials) {
  116. if (!credentials?.username || !credentials?.password) return null;
  117. const res = await fetch(LOGIN_API_PATH, {
  118. method: "POST",
  119. body: JSON.stringify(credentials),
  120. headers: { "Content-Type": "application/json" },
  121. });
  122. if (!res.ok) return null;
  123. const user = await res.json();
  124. // Important: next-auth expects the user object returned here
  125. // to be serializable and contain the fields you want in token/session
  126. // Ensure your backend returns: { id, accessToken, abilities, ...other fields }
  127. if (user && user.abilities) {
  128. return user; // this will be passed to jwt callback as `user`
  129. }
  130. return null;
  131. },
  132. }),
  133. ],
  134. pages: {
  135. signIn: "/login",
  136. },
  137. callbacks: {
  138. // Persist custom fields into the JWT token; refresh backend access JWT near expiry
  139. async jwt({ token, user, trigger, session }) {
  140. // First sign-in: `user` is available
  141. if (user) {
  142. token.id = user.id ?? token.sub; // fallback to sub if no id
  143. token.accessToken = user.accessToken;
  144. token.refreshToken = user.refreshToken;
  145. token.abilities = user.abilities ?? [];
  146. token.error = undefined;
  147. token.refreshFailedAt = undefined;
  148. const loginLocale = (user as { locale?: string }).locale;
  149. if (loginLocale) {
  150. token.locale = loginLocale;
  151. }
  152. return token;
  153. }
  154. if (trigger === "update" && session && typeof session === "object") {
  155. if ("locale" in session) {
  156. const next = (session as { locale?: string }).locale;
  157. if (next === "zh" || next === "en") {
  158. token.locale = next;
  159. }
  160. }
  161. if ("accessToken" in session && typeof (session as { accessToken?: string }).accessToken === "string") {
  162. token.accessToken = (session as { accessToken: string }).accessToken;
  163. }
  164. if ("refreshToken" in session && typeof (session as { refreshToken?: string }).refreshToken === "string") {
  165. token.refreshToken = (session as { refreshToken: string }).refreshToken;
  166. }
  167. }
  168. const expiresAt = backendAccessTokenExpiresAtMs(token.accessToken);
  169. // Can't parse exp → leave as-is (avoid refresh thrash). Still-valid → keep.
  170. if (expiresAt === null || Date.now() < expiresAt - ACCESS_TOKEN_REFRESH_SKEW_MS) {
  171. return token;
  172. }
  173. // Cooldown after a failed refresh so session polls don't hammer /refresh-token
  174. if (
  175. token.refreshFailedAt &&
  176. Date.now() - token.refreshFailedAt < REFRESH_FAILURE_COOLDOWN_MS
  177. ) {
  178. return token;
  179. }
  180. // Access token expired or near expiry — refresh using refresh token
  181. if (token.refreshToken) {
  182. return refreshAccessToken(token);
  183. }
  184. return token;
  185. },
  186. // Expose custom fields to the client session
  187. async session({ session, token }) {
  188. session.id = token.id as string | undefined;
  189. session.accessToken = token.accessToken as string | null;
  190. session.refreshToken = token.refreshToken as string | undefined;
  191. session.abilities = token.abilities as string[];
  192. session.exp = token.exp as number | undefined;
  193. session.locale = token.locale as string | undefined;
  194. session.error = token.error as string | undefined;
  195. // Also add abilities to session.user for easier client-side access
  196. if (session.user) {
  197. session.user.abilities = token.abilities as string[];
  198. }
  199. return session;
  200. },
  201. },
  202. };
  203. export type SessionWithTokens = Session & {
  204. accessToken: string | null;
  205. refreshToken?: string;
  206. abilities: string[];
  207. /** Backend / JWT subject — often numeric string or number */
  208. id?: string | number;
  209. locale?: string;
  210. };
  211. export default authOptions;