Merge pull request #574 from gitroomhq/feat/no-secured

Not secured
This commit is contained in:
Nevo David 2025-01-24 13:46:04 +07:00 committed by GitHub
commit 8b9f060188
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 356 additions and 122 deletions

View File

@ -31,7 +31,7 @@ export class AuthController {
@Get('/can-register')
async canRegister() {
return {register: await this._authService.canRegister()};
return { register: await this._authService.canRegister() };
}
@Post('/register')
@ -66,20 +66,36 @@ export class AuthController {
response.cookie('auth', jwt, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (process.env.NOT_SECURED) {
response.header('auth', jwt);
}
if (typeof addedOrg !== 'boolean' && addedOrg?.organizationId) {
response.cookie('showorg', addedOrg.organizationId, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (process.env.NOT_SECURED) {
response.header('showorg', addedOrg.organizationId);
}
}
response.header('onboarding', 'true');
@ -114,20 +130,36 @@ export class AuthController {
response.cookie('auth', jwt, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (process.env.NOT_SECURED) {
response.header('auth', jwt);
}
if (typeof addedOrg !== 'boolean' && addedOrg?.organizationId) {
response.cookie('showorg', addedOrg.organizationId, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (process.env.NOT_SECURED) {
response.header('showorg', addedOrg.organizationId);
}
}
response.header('reload', 'true');
@ -178,12 +210,20 @@ export class AuthController {
response.cookie('auth', activate, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (process.env.NOT_SECURED) {
response.header('auth', activate);
}
response.header('onboarding', 'true');
return response.status(200).send({ can: true });
}
@ -201,12 +241,20 @@ export class AuthController {
response.cookie('auth', jwt, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (process.env.NOT_SECURED) {
response.header('auth', jwt);
}
response.header('reload', 'true');
response.status(200).json({

View File

@ -101,8 +101,12 @@ export class PublicController {
if (!req.cookies.track) {
res.cookie('track', uniqueId, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
}
: {}),
sameSite: 'none',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
@ -111,8 +115,12 @@ export class PublicController {
if (body.fbclid && !req.cookies.fbclid) {
res.cookie('fbclid', body.fbclid, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
}
: {}),
sameSite: 'none',
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});

View File

@ -48,11 +48,13 @@ export class UsersController {
async getSelf(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() organization: Organization,
@Req() req: Request,
@Req() req: Request
) {
if (!organization) {
throw new HttpForbiddenException();
}
const impersonate = req.cookies.impersonate || req.headers.impersonate;
// @ts-ignore
return {
...user,
@ -67,12 +69,10 @@ export class UsersController {
// @ts-ignore
isLifetime: !!organization?.subscription?.isLifetime,
admin: !!user.isSuperAdmin,
impersonate: !!req.cookies.impersonate,
impersonate: !!impersonate,
allowTrial: organization?.allowTrial,
// @ts-ignore
publicApi: organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN'
? organization?.apiKey
: '',
publicApi: organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN' ? organization?.apiKey : '',
};
}
@ -105,11 +105,19 @@ export class UsersController {
response.cookie('impersonate', id, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (process.env.NOT_SECURED) {
response.header('impersonate', id);
}
}
@Post('/personal')
@ -175,12 +183,20 @@ export class UsersController {
) {
response.cookie('showorg', id, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (process.env.NOT_SECURED) {
response.header('showorg', id);
}
response.status(200).send();
}
@ -188,29 +204,41 @@ export class UsersController {
logout(@Res({ passthrough: true }) response: Response) {
response.cookie('auth', '', {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
maxAge: -1,
expires: new Date(0),
sameSite: 'none',
});
response.cookie('showorg', '', {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
maxAge: -1,
expires: new Date(0),
sameSite: 'none',
});
response.cookie('impersonate', '', {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
maxAge: -1,
expires: new Date(0),
sameSite: 'none',
});
response.status(200).send();
@ -223,22 +251,34 @@ export class UsersController {
@GetUserFromRequest() user: User,
@RealIP() ip: string,
@UserAgent() userAgent: string,
@Body() body: { tt: TrackEnum; fbclid: string, additional: Record<string, any> }
@Body()
body: { tt: TrackEnum; fbclid: string; additional: Record<string, any> }
) {
const uniqueId = req?.cookies?.track || makeId(10);
const fbclid = req?.cookies?.fbclid || body.fbclid;
await this._trackService.track(uniqueId, ip, userAgent, body.tt, body.additional, fbclid, user);
await this._trackService.track(
uniqueId,
ip,
userAgent,
body.tt,
body.additional,
fbclid,
user
);
if (!req.cookies.track) {
res.cookie('track', uniqueId, {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
}
console.log('hello');
res.status(200).json({
track: uniqueId,
});

View File

@ -14,8 +14,13 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule, {
rawBody: true,
cors: {
credentials: true,
exposedHeaders: ['reload', 'onboarding', 'activate'],
...(!process.env.NOT_SECURED ? { credentials: true } : {}),
exposedHeaders: [
'reload',
'onboarding',
'activate',
...(process.env.NOT_SECURED ? ['auth', 'showorg', 'impersonate'] : []),
],
origin: [
process.env.FRONTEND_URL,
...(process.env.MAIN_URL ? [process.env.MAIN_URL] : []),
@ -39,8 +44,8 @@ async function bootstrap() {
try {
await app.listen(port);
checkConfiguration() // Do this last, so that users will see obvious issues at the end of the startup log without having to scroll up.
checkConfiguration(); // Do this last, so that users will see obvious issues at the end of the startup log without having to scroll up.
Logger.log(`🚀 Backend is running on: http://localhost:${port}`);
} catch (e) {
@ -50,17 +55,17 @@ async function bootstrap() {
function checkConfiguration() {
const checker = new ConfigurationChecker();
checker.readEnvFromProcess()
checker.check()
checker.readEnvFromProcess();
checker.check();
if (checker.hasIssues()) {
for (const issue of checker.getIssues()) {
Logger.warn(issue, 'Configuration issue')
Logger.warn(issue, 'Configuration issue');
}
Logger.warn("Configuration issues found: " + checker.getIssuesCount())
Logger.warn('Configuration issues found: ' + checker.getIssuesCount());
} else {
Logger.log("Configuration check completed without any issues.")
Logger.log('Configuration check completed without any issues.');
}
}

View File

@ -10,9 +10,13 @@ import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/excep
export const removeAuth = (res: Response) => {
res.cookie('auth', '', {
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
secure: true,
httpOnly: true,
sameSite: 'none',
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: 'none',
}
: {}),
expires: new Date(0),
maxAge: -1,
});
@ -43,9 +47,10 @@ export class AuthMiddleware implements NestMiddleware {
throw new HttpForbiddenException();
}
if (user?.isSuperAdmin && req.cookies.impersonate) {
const impersonate = req.cookies.impersonate || req.headers.impersonate;
if (user?.isSuperAdmin && impersonate) {
const loadImpersonate = await this._organizationService.getUserOrg(
req.cookies.impersonate
impersonate
);
if (loadImpersonate) {

View File

@ -44,6 +44,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
telegramBotName={process.env.TELEGRAM_BOT_NAME!}
neynarClientId={process.env.NEYNAR_CLIENT_ID!}
isSecured={!process.env.NOT_SECURED}
>
<ToltScript />
<FacebookComponent />

View File

@ -6,6 +6,8 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { Select } from '@gitroom/react/form/select';
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { setCookie } from '@gitroom/frontend/components/layout/layout.context';
export const Subscription = () => {
const fetch = useFetch();
@ -53,6 +55,7 @@ export const Subscription = () => {
export const Impersonate = () => {
const fetch = useFetch();
const [name, setName] = useState('');
const { isSecured } = useVariables();
const user = useUser();
const load = useCallback(async () => {
@ -65,10 +68,14 @@ export const Impersonate = () => {
}, [name]);
const stopImpersonating = useCallback(async () => {
await fetch(`/user/impersonate`, {
method: 'POST',
body: JSON.stringify({ id: '' }),
});
if (!isSecured) {
setCookie('impersonate', '', -10);
} else {
await fetch(`/user/impersonate`, {
method: 'POST',
body: JSON.stringify({ id: '' }),
});
}
window.location.reload();
}, []);

View File

@ -14,13 +14,57 @@ export default function LayoutContext(params: { children: ReactNode }) {
return <></>;
}
export function setCookie(cname: string, cvalue: string, exdays: number) {
if (typeof document === 'undefined') {
return;
}
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
const expires = 'expires=' + d.toUTCString();
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/';
}
function LayoutContextInner(params: { children: ReactNode }) {
const returnUrl = useReturnUrl();
const {backendUrl, isGeneral} = useVariables();
const { backendUrl, isGeneral, isSecured } = useVariables();
const afterRequest = useCallback(
async (url: string, options: RequestInit, response: Response) => {
if (typeof window !== 'undefined' && window.location.href.includes('/p/')) {
if (
typeof window !== 'undefined' &&
window.location.href.includes('/p/')
) {
return true;
}
const headerAuth =
response?.headers?.get('auth') || response?.headers?.get('Auth');
const showOrg =
response?.headers?.get('showorg') || response?.headers?.get('Showorg');
const impersonate =
response?.headers?.get('impersonate') ||
response?.headers?.get('Impersonate');
const logout =
response?.headers?.get('logout') || response?.headers?.get('Logout');
if (headerAuth) {
setCookie('auth', headerAuth, 365);
}
if (showOrg) {
setCookie('showorg', showOrg, 365);
}
if (impersonate) {
setCookie('impersonate', impersonate, 365);
}
if (logout && !isSecured) {
setCookie('auth', '', -10);
setCookie('showorg', '', -10);
setCookie('impersonate', '', -10);
window.location.href = '/';
return true;
}
@ -50,6 +94,11 @@ function LayoutContextInner(params: { children: ReactNode }) {
}
if (response.status === 401) {
if (!isSecured) {
setCookie('auth', '', -10);
setCookie('showorg', '', -10);
setCookie('impersonate', '', -10);
}
window.location.href = '/';
}
@ -74,10 +123,7 @@ function LayoutContextInner(params: { children: ReactNode }) {
);
return (
<FetchWrapperComponent
baseUrl={backendUrl}
afterRequest={afterRequest}
>
<FetchWrapperComponent baseUrl={backendUrl} afterRequest={afterRequest}>
{params?.children || <></>}
</FetchWrapperComponent>
);

View File

@ -4,19 +4,28 @@ import { useCallback } from 'react';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { setCookie } from '@gitroom/frontend/components/layout/layout.context';
export const LogoutComponent = () => {
const fetch = useFetch();
const {isGeneral} = useVariables();
const { isGeneral, isSecured } = useVariables();
const logout = useCallback(async () => {
if (await deleteDialog('Are you sure you want to logout?', 'Yes logout')) {
await fetch('/user/logout', {
method: 'POST',
});
if (!isSecured) {
setCookie('auth', '', -10);
} else {
await fetch('/user/logout', {
method: 'POST',
});
}
window.location.href = '/';
}
}, []);
return <div className="text-red-400 cursor-pointer" onClick={logout}>Logout from {isGeneral ? 'Postiz' : 'Gitroom'}</div>;
return (
<div className="text-red-400 cursor-pointer" onClick={logout}>
Logout from {isGeneral ? 'Postiz' : 'Gitroom'}
</div>
);
};

View File

@ -7,7 +7,11 @@ import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
export async function middleware(request: NextRequest) {
const nextUrl = request.nextUrl;
const authCookie = request.cookies.get('auth');
if (nextUrl.pathname.startsWith('/uploads/') || nextUrl.pathname.startsWith('/p/') || nextUrl.pathname.startsWith('/icons/')) {
if (
nextUrl.pathname.startsWith('/uploads/') ||
nextUrl.pathname.startsWith('/p/') ||
nextUrl.pathname.startsWith('/icons/')
) {
return NextResponse.next();
}
// If the URL is logout, delete the cookie and redirect to login
@ -17,9 +21,13 @@ export async function middleware(request: NextRequest) {
);
response.cookies.set('auth', '', {
path: '/',
sameSite: false,
httpOnly: true,
secure: true,
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: false,
}
: {}),
maxAge: -1,
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
});
@ -53,12 +61,16 @@ export async function middleware(request: NextRequest) {
if (org) {
const redirect = NextResponse.redirect(new URL(`/`, nextUrl.href));
redirect.cookies.set('org', org, {
path: '/',
sameSite: false,
httpOnly: true,
secure: true,
...(!process.env.NOT_SECURED
? {
path: '/',
secure: true,
httpOnly: true,
sameSite: false,
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
}
: {}),
expires: new Date(Date.now() + 15 * 60 * 1000),
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
});
return redirect;
}
@ -81,12 +93,16 @@ export async function middleware(request: NextRequest) {
);
if (id) {
redirect.cookies.set('showorg', id, {
path: '/',
sameSite: false,
httpOnly: true,
secure: true,
...(!process.env.NOT_SECURED
? {
path: '/',
secure: true,
httpOnly: true,
sameSite: false,
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
}
: {}),
expires: new Date(Date.now() + 15 * 60 * 1000),
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
});
}
@ -112,9 +128,13 @@ export async function middleware(request: NextRequest) {
next.cookies.set('marketplace', type === 'seller' ? 'seller' : 'buyer', {
path: '/',
sameSite: false,
httpOnly: true,
secure: true,
...(!process.env.NOT_SECURED
? {
secure: true,
httpOnly: true,
sameSite: false,
}
: {}),
expires: new Date(Date.now() + 15 * 60 * 1000),
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
});
@ -122,6 +142,7 @@ export async function middleware(request: NextRequest) {
return next;
} catch (err) {
console.log('err', err);
return NextResponse.redirect(new URL('/auth/logout', nextUrl.href));
}
}

View File

@ -1,4 +1,3 @@
export interface Params {
baseUrl: string;
beforeRequest?: (url: string, options: RequestInit) => Promise<RequestInit>;
@ -11,21 +10,48 @@ export interface Params {
export const customFetch = (
params: Params,
auth?: string,
showorg?: string
showorg?: string,
secured: boolean = true
) => {
return async function newFetch(url: string, options: RequestInit = {}) {
const newRequestObject = await params?.beforeRequest?.(url, options);
const authNonSecuredCookie = typeof document === 'undefined' ? null : document.cookie
.split(';')
.find((p) => p.includes('auth='))
?.split('=')[1];
const authNonSecuredOrg = typeof document === 'undefined' ? null : document.cookie
.split(';')
.find((p) => p.includes('showorg='))
?.split('=')[1];
const authNonSecuredImpersonate = typeof document === 'undefined' ? null : document.cookie
.split(';')
.find((p) => p.includes('impersonate='))
?.split('=')[1];
const fetchRequest = await fetch(params.baseUrl + url, {
credentials: 'include',
...(secured ? { credentials: 'include' } : {}),
...(newRequestObject || options),
headers: {
...(auth ? { auth } : {}),
...(showorg ? { showorg } : {}),
...(showorg
? { showorg }
: authNonSecuredOrg
? { showorg: authNonSecuredOrg }
: {}),
...(options.body instanceof FormData
? {}
: { 'Content-Type': 'application/json' }),
Accept: 'application/json',
...options?.headers,
...(auth
? { auth }
: authNonSecuredCookie
? { auth: authNonSecuredCookie }
: {}),
...(authNonSecuredImpersonate
? { impersonate: authNonSecuredImpersonate }
: {}),
},
// @ts-ignore
...(!options.next && options.cache !== 'force-cache'

View File

@ -1,30 +1,46 @@
"use client";
'use client';
import {createContext, FC, ReactNode, useContext, useRef, useState} from "react";
import {customFetch, Params} from "./custom.fetch.func";
import {
createContext,
FC,
ReactNode,
useContext,
useRef,
useState,
} from 'react';
import { customFetch, Params } from './custom.fetch.func';
import { useVariables } from '@gitroom/react/helpers/variable.context';
const FetchProvider = createContext(customFetch(
const FetchProvider = createContext(
customFetch(
// @ts-ignore
{
baseUrl: '',
beforeRequest: () => {},
afterRequest: () => {
return true;
}
} as Params));
baseUrl: '',
beforeRequest: () => {},
afterRequest: () => {
return true;
},
} as Params
)
);
export const FetchWrapperComponent: FC<Params & {children: ReactNode}> = (props) => {
const {children, ...params} = props;
export const FetchWrapperComponent: FC<Params & { children: ReactNode }> = (
props
) => {
const { children, ...params } = props;
const { isSecured } = useVariables();
// @ts-ignore
const fetchData = useRef(
customFetch(params, undefined, undefined, isSecured)
);
return (
// @ts-ignore
const fetchData = useRef(customFetch(params));
return (
// @ts-ignore
<FetchProvider.Provider value={fetchData.current}>
{children}
</FetchProvider.Provider>
)
}
<FetchProvider.Provider value={fetchData.current}>
{children}
</FetchProvider.Provider>
);
};
export const useFetch = () => {
return useContext(FetchProvider);
}
return useContext(FetchProvider);
};

View File

@ -14,6 +14,7 @@ interface VariableContextInterface {
facebookPixel: string;
telegramBotName: string;
neynarClientId: string;
isSecured: boolean;
tolt: string;
}
const VariableContext = createContext({
@ -25,6 +26,7 @@ const VariableContext = createContext({
backendUrl: '',
discordUrl: '',
uploadDirectory: '',
isSecured: false,
telegramBotName: '',
facebookPixel: '',
neynarClientId: '',