43 | Reddit limits how frequently we can request data. Retrying 44 | automatically in {timeLeft}{" "} 45 | seconds... 46 |
47 |{message}
58 |{description}
59 |103 | {isRetrying 104 | ? `Authenticating attempt ${ 105 | attempts + 1 106 | } of ${MAX_RETRY_ATTEMPTS}...` 107 | : "Authenticating with Reddit..."} 108 |
109 |{error}
120 | 121 |Redirecting to saved posts...
147 |62 | Organize, filter, and rediscover your saved Reddit content with ease 63 |
64 | 65 | 68 |151 | Your data never leaves your browser. We don't store any of your 152 | Reddit information on our servers. 153 |
154 |Reddit's missing save manager: Finally organize what matters to you
6 |A modern tool to organize, search, and manage your Reddit saved posts and comments.
7 |8 | Live Demo | 9 | Features | 10 | Getting Started | 11 | Deployment 12 |
13 |205 | Your data, better organized. Never lose a valuable Reddit post again. 206 |
207 | -------------------------------------------------------------------------------- /server/src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Authentication controller for Reddit OAuth flow 3 | * Handles token exchange and token refresh operations 4 | */ 5 | import { Request, Response } from "express"; 6 | import fetch from "node-fetch"; 7 | import { formatErrorResponse } from "../utils/responses.js"; 8 | import { logInfo, logError, logWarn } from "../utils/logger.js"; 9 | import { getSecret } from "../utils/getSecret.js"; 10 | 11 | /** 12 | * Handle token exchange from authorization code 13 | * Converts the OAuth code received from Reddit into access and refresh tokens 14 | * 15 | * @param req Express request object containing code and redirectUri 16 | * @param res Express response object 17 | */ 18 | export async function exchangeToken(req: Request, res: Response) { 19 | try { 20 | const { code, redirectUri } = req.body; 21 | // Use getSecret for prod, fallback to env for dev 22 | const clientId = getSecret("REDDIT_CLIENT_ID"); 23 | const clientSecret = getSecret("REDDIT_CLIENT_SECRET"); 24 | 25 | // Validate required parameters 26 | if (!code || !redirectUri) { 27 | logError("Missing parameters in token exchange request", null, { 28 | body: req.body, 29 | missingCode: !code, 30 | missingRedirectUri: !redirectUri, 31 | }); 32 | 33 | return res 34 | .status(400) 35 | .json( 36 | formatErrorResponse( 37 | 400, 38 | "Missing required parameters. Both code and redirectUri are required." 39 | ) 40 | ); 41 | } 42 | 43 | // Validate server configuration 44 | if (!clientId || !clientSecret) { 45 | logError( 46 | "Missing server environment variables for Reddit API credentials", 47 | null, 48 | { 49 | missingClientId: !clientId, 50 | missingClientSecret: !clientSecret, 51 | } 52 | ); 53 | 54 | return res 55 | .status(500) 56 | .json( 57 | formatErrorResponse( 58 | 500, 59 | "Server configuration error: Missing API credentials" 60 | ) 61 | ); 62 | } 63 | 64 | logInfo(`Attempting token exchange with Reddit`, { 65 | redirectUri, 66 | hasCode: !!code, 67 | }); 68 | 69 | // Create Base64 encoded credentials for Basic Auth 70 | const encodedCredentials = Buffer.from( 71 | `${clientId}:${clientSecret}` 72 | ).toString("base64"); 73 | 74 | // Make the token exchange request to Reddit 75 | const response = await fetch("https://www.reddit.com/api/v1/access_token", { 76 | method: "POST", 77 | headers: { 78 | Authorization: `Basic ${encodedCredentials}`, 79 | "Content-Type": "application/x-www-form-urlencoded", 80 | "User-Agent": process.env.USER_AGENT || "bookmarkeddit/1.0", 81 | }, 82 | body: `grant_type=authorization_code&code=${encodeURIComponent( 83 | code 84 | )}&redirect_uri=${encodeURIComponent(redirectUri)}`, 85 | }); 86 | 87 | // Handle error responses from Reddit API 88 | if (!response.ok) { 89 | const errorText = await response.text(); 90 | logError("Token exchange failed with Reddit API", null, { 91 | status: response.status, 92 | errorText, 93 | }); 94 | 95 | return res 96 | .status(response.status) 97 | .json( 98 | formatErrorResponse( 99 | response.status, 100 | `Failed to exchange token: ${errorText}` 101 | ) 102 | ); 103 | } 104 | 105 | // Process successful response 106 | const data = (await response.json()) as Record186 | {post.description} 187 |
188 | 189 | 190 | {/* Display media content with video taking priority over images */} 191 | {store.showImages && ( 192 | <> 193 | {post.video ? ( 194 |