Sfoglia il codice sorgente

refactor: cleanup now un-used graphql resolvers and services

Nicolas Meienberger 2 anni fa
parent
commit
4609078894

+ 2 - 1
package.json

@@ -12,7 +12,8 @@
     "start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres",
     "version": "echo $npm_package_version",
     "release:rc": "./scripts/deploy/release-rc.sh",
-    "test:build": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t meienberger/runtipi:test ."
+    "test:build": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t meienberger/runtipi:test .",
+    "test:build:arm64": "docker buildx build --platform linux/arm64 -t meienberger/runtipi:test ."
   },
   "devDependencies": {
     "@commitlint/cli": "^17.0.3",

+ 1 - 0
packages/dashboard/package.json

@@ -26,6 +26,7 @@
     "@trpc/next": "^10.7.0",
     "@trpc/react-query": "^10.7.0",
     "@trpc/server": "^10.7.0",
+    "argon2": "^0.29.1",
     "clsx": "^1.1.1",
     "fs-extra": "^10.1.0",
     "graphql": "^15.8.0",

+ 9 - 1
packages/system-api/.env.test

@@ -1,3 +1,11 @@
+ROOT_FOLDER=/test
+ROOT_FOLDER_HOST=/tipi
+JWT_SECRET=secret
+POSTGRES_DBNAME=postgres
+POSTGRES_HOST=localhost
+POSTGRES_USERNAME=postgres
+POSTGRES_PASSWORD=postgres
 APPS_REPO_ID=repo-id
+APPS_REPO_URL=http://test.com
 INTERNAL_IP=localhost
-JWT_SECRET=secret
+STORAGE_PATH=/tipi/test

+ 0 - 234
packages/system-api/src/modules/auth/__tests__/auth.resolver.test.ts

@@ -1,234 +0,0 @@
-import { faker } from '@faker-js/faker';
-import jwt from 'jsonwebtoken';
-import { DataSource } from 'typeorm';
-import TipiCache from '../../../config/TipiCache';
-import { getConfig } from '../../../core/config/TipiConfig';
-import { setupConnection, teardownConnection } from '../../../test/connection';
-import { gcall } from '../../../test/gcall';
-import { loginMutation, registerMutation } from '../../../test/mutations';
-import { isConfiguredQuery, MeQuery, refreshTokenQuery } from '../../../test/queries';
-import { TokenResponse } from '../auth.types';
-import User from '../user.entity';
-import { createUser } from './user.factory';
-
-jest.mock('redis');
-
-let db: DataSource | null = null;
-const TEST_SUITE = 'authresolver';
-
-beforeAll(async () => {
-  db = await setupConnection(TEST_SUITE);
-});
-
-afterAll(async () => {
-  await db?.destroy();
-  await teardownConnection(TEST_SUITE);
-});
-
-beforeEach(async () => {
-  jest.resetModules();
-  jest.resetAllMocks();
-  jest.restoreAllMocks();
-  await User.clear();
-});
-
-describe('Test: me', () => {
-  const email = faker.internet.email();
-  let user1: User;
-
-  beforeEach(async () => {
-    user1 = await createUser(email);
-  });
-
-  it('should return null if no user is logged in', async () => {
-    const { data } = await gcall<{ me: User }>({
-      source: MeQuery,
-    });
-
-    expect(data?.me).toBeNull();
-  });
-
-  it('should return the user if a user is logged in', async () => {
-    const { data } = await gcall<{ me: User | null }>({
-      source: MeQuery,
-      userId: user1.id,
-    });
-
-    expect(data?.me?.username).toEqual(user1.username);
-  });
-});
-
-describe('Test: register', () => {
-  const email = faker.internet.email();
-  const password = faker.internet.password();
-
-  it('should register a user', async () => {
-    const { data } = await gcall<{ register: TokenResponse }>({
-      source: registerMutation,
-      variableValues: {
-        input: { username: email, password },
-      },
-    });
-
-    expect(data?.register).toBeDefined();
-    expect(data?.register?.token).toBeDefined();
-
-    const decoded = jwt.verify(data?.register?.token || '', getConfig().jwtSecret) as jwt.JwtPayload;
-
-    expect(decoded).toBeDefined();
-    expect(decoded).not.toBeNull();
-    expect(decoded).toHaveProperty('id');
-    expect(decoded).toHaveProperty('iat');
-    expect(decoded).toHaveProperty('exp');
-    expect(decoded).toHaveProperty('session');
-  });
-
-  it('should not register a user with an existing username', async () => {
-    await createUser(email);
-
-    const { errors } = await gcall<{ register: TokenResponse }>({
-      source: registerMutation,
-      variableValues: {
-        input: { username: email, password },
-      },
-    });
-
-    expect(errors?.[0].message).toEqual('User already exists');
-  });
-
-  it('should not register a user with a malformed email', async () => {
-    const { errors } = await gcall<{ register: TokenResponse }>({
-      source: registerMutation,
-      variableValues: {
-        input: { username: 'not an email', password },
-      },
-    });
-
-    expect(errors?.[0].message).toEqual('Invalid username');
-  });
-});
-
-describe('Test: login', () => {
-  const email = faker.internet.email();
-
-  beforeEach(async () => {
-    await createUser(email);
-  });
-
-  it('should login a user', async () => {
-    const { data } = await gcall<{ login: TokenResponse }>({
-      source: loginMutation,
-      variableValues: {
-        input: { username: email, password: 'password' },
-      },
-    });
-
-    const token = data?.login.token as string;
-
-    expect(token).toBeDefined();
-
-    const decoded = jwt.verify(token, getConfig().jwtSecret) as { id: string; session: string };
-
-    const user = await User.findOne({ where: { username: email.toLowerCase().trim() } });
-
-    expect(decoded.id).toBeDefined();
-    expect(user?.id).toEqual(decoded.id);
-  });
-
-  it('should not login a user with an incorrect password', async () => {
-    const { errors } = await gcall<{ login: TokenResponse }>({
-      source: loginMutation,
-      variableValues: {
-        input: { username: email, password: 'wrong password' },
-      },
-    });
-
-    expect(errors?.[0].message).toEqual('Wrong password');
-  });
-
-  it('should not login a user with a malformed email', async () => {
-    const { errors } = await gcall<{ login: TokenResponse }>({
-      source: loginMutation,
-      variableValues: {
-        input: { username: 'not an email', password: 'password' },
-      },
-    });
-
-    expect(errors?.[0].message).toEqual('User not found');
-  });
-});
-
-describe('Test: logout', () => {
-  const email = faker.internet.email();
-  let user1: User;
-
-  beforeEach(async () => {
-    user1 = await createUser(email);
-  });
-
-  it('should logout a user', async () => {
-    const { data } = await gcall<{ logout: boolean }>({
-      source: 'mutation { logout }',
-      userId: user1.id,
-      session: 'session',
-    });
-
-    expect(data?.logout).toBeTruthy();
-  });
-});
-
-describe('Test: isConfigured', () => {
-  it('should return false if no users exist', async () => {
-    const { data } = await gcall<{ isConfigured: boolean }>({
-      source: isConfiguredQuery,
-    });
-
-    expect(data?.isConfigured).toBeFalsy();
-  });
-
-  it('should return true if a user exists', async () => {
-    await createUser(faker.internet.email());
-
-    const { data } = await gcall<{ isConfigured: boolean }>({
-      source: isConfiguredQuery,
-    });
-
-    expect(data?.isConfigured).toBeTruthy();
-  });
-});
-
-describe('Test: refreshToken', () => {
-  const email = faker.internet.email();
-  let user1: User;
-
-  beforeEach(async () => {
-    user1 = await createUser(email);
-  });
-
-  it('should return a new token', async () => {
-    // Arrange
-    const session = faker.datatype.uuid();
-    await TipiCache.set(session, user1.id.toString());
-
-    // Act
-    const { data } = await gcall<{ refreshToken: TokenResponse }>({
-      source: refreshTokenQuery,
-      userId: user1.id,
-      session,
-    });
-    const decoded = jwt.verify(data?.refreshToken?.token || '', getConfig().jwtSecret) as jwt.JwtPayload;
-
-    // Assert
-    expect(data?.refreshToken).toBeDefined();
-    expect(data?.refreshToken?.token).toBeDefined();
-    expect(decoded).toBeDefined();
-    expect(decoded).not.toBeNull();
-    expect(decoded).toHaveProperty('id');
-    expect(decoded).toHaveProperty('iat');
-    expect(decoded).toHaveProperty('exp');
-    expect(decoded).toHaveProperty('session');
-
-    expect(decoded.id).toEqual(user1.id.toString());
-    expect(decoded.session).not.toEqual(session);
-  });
-});

+ 0 - 234
packages/system-api/src/modules/auth/__tests__/auth.service.test.ts

@@ -1,234 +0,0 @@
-import * as argon2 from 'argon2';
-import jwt from 'jsonwebtoken';
-import { faker } from '@faker-js/faker';
-import { DataSource } from 'typeorm';
-import AuthService from '../auth.service';
-import { createUser } from './user.factory';
-import User from '../user.entity';
-import { setupConnection, teardownConnection } from '../../../test/connection';
-import { setConfig } from '../../../core/config/TipiConfig';
-import TipiCache from '../../../config/TipiCache';
-
-let db: DataSource | null = null;
-const TEST_SUITE = 'authservice';
-
-jest.mock('redis');
-
-beforeAll(async () => {
-  setConfig('jwtSecret', 'test');
-  db = await setupConnection(TEST_SUITE);
-});
-
-beforeEach(async () => {
-  await User.clear();
-});
-
-afterAll(async () => {
-  await db?.destroy();
-  await teardownConnection(TEST_SUITE);
-});
-
-describe('Login', () => {
-  it('Should return a valid jsonwebtoken containing a user id', async () => {
-    // Arrange
-    const email = faker.internet.email();
-    const user = await createUser(email);
-
-    // Act
-    const { token } = await AuthService.login({ username: email, password: 'password' });
-    const decoded = jwt.verify(token, 'test') as jwt.JwtPayload;
-
-    // Assert
-    expect(decoded).toBeDefined();
-    expect(decoded).toBeDefined();
-    expect(decoded).not.toBeNull();
-    expect(decoded).toHaveProperty('id');
-    expect(decoded.id).toBe(user.id);
-    expect(decoded).toHaveProperty('iat');
-    expect(decoded).toHaveProperty('exp');
-    expect(decoded).toHaveProperty('session');
-  });
-
-  it('Should throw if user does not exist', async () => {
-    await expect(AuthService.login({ username: 'test', password: 'test' })).rejects.toThrowError('User not found');
-  });
-
-  it('Should throw if password is incorrect', async () => {
-    const email = faker.internet.email();
-    await createUser(email);
-    await expect(AuthService.login({ username: email, password: 'wrong' })).rejects.toThrowError('Wrong password');
-  });
-});
-
-describe('Register', () => {
-  it('Should return valid jsonwebtoken after register', async () => {
-    // Arrange
-    const email = faker.internet.email();
-
-    // Act
-    const { token } = await AuthService.register({ username: email, password: 'password' });
-    const decoded = jwt.verify(token, 'test') as jwt.JwtPayload;
-
-    // Assert
-    expect(decoded).toBeDefined();
-    expect(decoded).not.toBeNull();
-    expect(decoded).toHaveProperty('id');
-    expect(decoded).toHaveProperty('iat');
-    expect(decoded).toHaveProperty('exp');
-    expect(decoded).toHaveProperty('session');
-  });
-
-  it('Should correctly trim and lowercase email', async () => {
-    // Arrange
-    const email = faker.internet.email();
-
-    // Act
-    await AuthService.register({ username: email, password: 'test' });
-    const user = await User.findOne({ where: { username: email.toLowerCase().trim() } });
-
-    // Assert
-    expect(user).toBeDefined();
-    expect(user?.username).toBe(email.toLowerCase().trim());
-  });
-
-  it('Should throw if user already exists', async () => {
-    // Arrange
-    const email = faker.internet.email();
-
-    // Act & Assert
-    await createUser(email);
-    await expect(AuthService.register({ username: email, password: 'test' })).rejects.toThrowError('User already exists');
-  });
-
-  it('Should throw if email is not provided', async () => {
-    await expect(AuthService.register({ username: '', password: 'test' })).rejects.toThrowError('Missing email or password');
-  });
-
-  it('Should throw if password is not provided', async () => {
-    await expect(AuthService.register({ username: faker.internet.email(), password: '' })).rejects.toThrowError('Missing email or password');
-  });
-
-  it('Password is correctly hashed', async () => {
-    // Arrange
-    const email = faker.internet.email().toLowerCase().trim();
-
-    // Act
-    await AuthService.register({ username: email, password: 'test' });
-    const user = await User.findOne({ where: { username: email } });
-    const isPasswordValid = await argon2.verify(user?.password || '', 'test');
-
-    // Assert
-    expect(isPasswordValid).toBe(true);
-  });
-
-  it('Should throw if email is invalid', async () => {
-    await expect(AuthService.register({ username: 'test', password: 'test' })).rejects.toThrowError('Invalid username');
-  });
-});
-
-describe('Test: logout', () => {
-  it('Should return true if there is no session to delete', async () => {
-    // Act
-    // @ts-ignore
-    const result = await AuthService.logout();
-
-    // Assert
-    expect(result).toBe(true);
-  });
-
-  it('Should delete session from cache', async () => {
-    // Arrange
-    const session = faker.random.alphaNumeric(32);
-    await TipiCache.set(session, 'test');
-    expect(await TipiCache.get(session)).toBe('test');
-
-    // Act
-    const result = await AuthService.logout(session);
-
-    // Assert
-    expect(result).toBe(true);
-    expect(await TipiCache.get('session')).toBeUndefined();
-  });
-});
-
-describe('Test: refreshToken', () => {
-  it('Should return null if session is not provided', async () => {
-    // Act
-    const result = await AuthService.refreshToken();
-
-    // Assert
-    expect(result).toBeNull();
-  });
-
-  it('Should return null if session is not found in cache', async () => {
-    // Act
-    const result = await AuthService.refreshToken('test');
-
-    // Assert
-    expect(result).toBeNull();
-  });
-
-  it('Should return a new token if session is found in cache', async () => {
-    // Arrange
-    const session = faker.random.alphaNumeric(32);
-    await TipiCache.set(session, 'test');
-
-    // Act
-    const result = await AuthService.refreshToken(session);
-
-    // Assert
-    expect(result).not.toBeNull();
-    expect(result).toHaveProperty('token');
-    expect(result?.token).not.toBe(session);
-  });
-
-  it('Should put expiration in 6 seconds for old session', async () => {
-    // Arrange
-    const session = faker.random.alphaNumeric(32);
-    await TipiCache.set(session, '1');
-
-    // Act
-    const result = await AuthService.refreshToken(session);
-    const expiration = await TipiCache.ttl(session);
-
-    // Assert
-    expect(result).not.toBeNull();
-    expect(result).toHaveProperty('token');
-    expect(result?.token).not.toBe(session);
-    expect(expiration).toMatchObject({ EX: 6 });
-  });
-});
-
-describe('Test: me', () => {
-  it('Should return null if userId is not provided', async () => {
-    // Act
-    const result = await AuthService.me();
-
-    // Assert
-    expect(result).toBeNull();
-  });
-
-  it('Should return null if user does not exist', async () => {
-    // Act
-    const result = await AuthService.me(1);
-
-    // Assert
-    expect(result).toBeNull();
-  });
-
-  it('Should return user if user exists', async () => {
-    // Arrange
-    const email = faker.internet.email();
-    const user = await createUser(email);
-
-    // Act
-    const result = await AuthService.me(user.id);
-
-    // Assert
-    expect(result).not.toBeNull();
-    expect(result).toHaveProperty('id');
-    expect(result).toHaveProperty('username');
-    expect(result).toHaveProperty('createdAt');
-    expect(result).toHaveProperty('updatedAt');
-  });
-});

+ 0 - 54
packages/system-api/src/modules/auth/auth.resolver.ts

@@ -1,54 +0,0 @@
-import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql';
-import { MyContext } from '../../types';
-import { TokenResponse, UsernamePasswordInput } from './auth.types';
-
-import AuthService from './auth.service';
-import User from './user.entity';
-
-@Resolver()
-export default class AuthResolver {
-  @Query(() => User, { nullable: true })
-  async me(@Ctx() ctx: MyContext): Promise<User | null> {
-    return AuthService.me(ctx.req?.session?.userId);
-  }
-
-  @Mutation(() => TokenResponse)
-  async register(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput): Promise<TokenResponse> {
-    const { token } = await AuthService.register(input);
-
-    return { token };
-  }
-
-  @Mutation(() => TokenResponse)
-  async login(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput): Promise<TokenResponse> {
-    const { token } = await AuthService.login(input);
-
-    return { token };
-  }
-
-  @Mutation(() => Boolean)
-  async logout(@Ctx() { req }: MyContext): Promise<boolean> {
-    if (req.session.id) {
-      await AuthService.logout(req.session?.id);
-    }
-
-    req.session.userId = undefined;
-    req.session.id = undefined;
-
-    return true;
-  }
-
-  @Query(() => Boolean)
-  async isConfigured(): Promise<boolean> {
-    const users = await User.find();
-
-    return users.length > 0;
-  }
-
-  @Query(() => TokenResponse, { nullable: true })
-  async refreshToken(@Ctx() { req }: MyContext): Promise<TokenResponse | null> {
-    const res = await AuthService.refreshToken(req.session?.id);
-
-    return res;
-  }
-}

+ 0 - 102
packages/system-api/src/modules/auth/auth.service.ts

@@ -1,102 +0,0 @@
-import * as argon2 from 'argon2';
-import { v4 } from 'uuid';
-import jwt from 'jsonwebtoken';
-import validator from 'validator';
-import { getConfig } from '../../core/config/TipiConfig';
-import { TokenResponse, UsernamePasswordInput } from './auth.types';
-import User from './user.entity';
-import TipiCache from '../../config/TipiCache';
-
-const login = async (input: UsernamePasswordInput): Promise<TokenResponse> => {
-  const { password, username } = input;
-
-  const user = await User.findOne({ where: { username: username.trim().toLowerCase() } });
-
-  if (!user) {
-    throw new Error('User not found');
-  }
-
-  const isPasswordValid = await argon2.verify(user.password, password);
-
-  if (!isPasswordValid) {
-    throw new Error('Wrong password');
-  }
-
-  const session = v4();
-  const token = jwt.sign({ id: user.id, session }, getConfig().jwtSecret, { expiresIn: '7d' });
-
-  await TipiCache.set(session, user.id.toString());
-
-  return { token };
-};
-
-const register = async (input: UsernamePasswordInput): Promise<TokenResponse> => {
-  const { password, username } = input;
-  const email = username.trim().toLowerCase();
-
-  if (!username || !password) {
-    throw new Error('Missing email or password');
-  }
-
-  if (username.length < 3 || !validator.isEmail(email)) {
-    throw new Error('Invalid username');
-  }
-
-  const user = await User.findOne({ where: { username: email } });
-
-  if (user) {
-    throw new Error('User already exists');
-  }
-
-  const hash = await argon2.hash(password);
-  const newUser = await User.create({ username: email, password: hash }).save();
-
-  const session = v4();
-  const token = jwt.sign({ id: newUser.id, session }, getConfig().jwtSecret, { expiresIn: '1d' });
-
-  await TipiCache.set(session, newUser.id.toString());
-
-  return { token };
-};
-
-const me = async (userId?: number): Promise<User | null> => {
-  if (!userId) return null;
-
-  const user = await User.findOne({ where: { id: userId } });
-
-  if (!user) return null;
-
-  return user;
-};
-
-const logout = async (session: string): Promise<boolean> => {
-  await TipiCache.del(session);
-
-  return true;
-};
-
-const refreshToken = async (session?: string): Promise<TokenResponse | null> => {
-  if (!session) return null;
-
-  const userId = await TipiCache.get(session);
-  if (!userId) return null;
-
-  // Expire token in 6 seconds
-  await TipiCache.set(session, userId, 6);
-
-  const newSession = v4();
-  const token = jwt.sign({ id: userId, session: newSession }, getConfig().jwtSecret, { expiresIn: '1d' });
-  await TipiCache.set(newSession, userId);
-
-  return { token };
-};
-
-const AuthService = {
-  login,
-  register,
-  me,
-  logout,
-  refreshToken,
-};
-
-export default AuthService;

+ 0 - 18
packages/system-api/src/modules/auth/auth.types.ts

@@ -1,18 +0,0 @@
-import { Field, InputType, ObjectType } from 'type-graphql';
-
-@InputType()
-class UsernamePasswordInput {
-  @Field(() => String)
-  username!: string;
-
-  @Field(() => String)
-  password!: string;
-}
-
-@ObjectType()
-class TokenResponse {
-  @Field(() => String, { nullable: false })
-  token!: string;
-}
-
-export { UsernamePasswordInput, TokenResponse };

+ 1 - 2
packages/system-api/src/schema.ts

@@ -2,11 +2,10 @@ import { GraphQLSchema } from 'graphql';
 import { buildSchema } from 'type-graphql';
 import { customAuthChecker } from './core/middlewares/authChecker';
 import AppsResolver from './modules/apps/apps.resolver';
-import AuthResolver from './modules/auth/auth.resolver';
 
 const createSchema = (): Promise<GraphQLSchema> =>
   buildSchema({
-    resolvers: [AppsResolver, AuthResolver],
+    resolvers: [AppsResolver],
     validate: true,
     authChecker: customAuthChecker,
   });

+ 3 - 0
pnpm-lock.yaml

@@ -51,6 +51,7 @@ importers:
       '@types/validator': ^13.7.2
       '@typescript-eslint/eslint-plugin': ^5.47.1
       '@typescript-eslint/parser': ^5.47.1
+      argon2: ^0.29.1
       clsx: ^1.1.1
       dotenv-cli: ^6.0.0
       eslint: 8.30.0
@@ -91,6 +92,7 @@ importers:
       ts-node: ^10.9.1
       tslib: ^2.4.0
       typescript: 4.9.4
+      uuid: ^9.0.0
       validator: ^13.7.0
       whatwg-fetch: ^3.6.2
       winston: ^3.7.2
@@ -107,6 +109,7 @@ importers:
       '@trpc/next': 10.7.0_gmaturtqmtlrknaw65ek5pmv2i
       '@trpc/react-query': 10.7.0_x4ie6nhblo2vtx2aafrgzlfqy4
       '@trpc/server': 10.7.0
+      argon2: 0.29.1
       clsx: 1.1.1
       fs-extra: 10.1.0
       graphql: 15.8.0