Repository: Benjamin-Code-YouTube/boumboum-back Branch: main Commit: 4254ef2be844 Files: 67 Total size: 105.7 KB Directory structure: gitextract_bylbss8l/ ├── .adonisrc.json ├── .dockerignore ├── .editorconfig ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── Dockerfile ├── README.md ├── ace ├── ace-manifest.json ├── app/ │ ├── Controllers/ │ │ └── Http/ │ │ ├── GendersController.ts │ │ ├── MatchesController.ts │ │ ├── ProfilesController.ts │ │ ├── SpotifyController.ts │ │ └── UsersController.ts │ ├── Exceptions/ │ │ └── Handler.ts │ ├── Middleware/ │ │ ├── Auth.ts │ │ └── SilentAuth.ts │ ├── Models/ │ │ ├── ApiToken.ts │ │ ├── Artist.ts │ │ ├── Gender.ts │ │ ├── Genre.ts │ │ ├── Match.ts │ │ ├── Profile.ts │ │ ├── SocialToken.ts │ │ ├── Track.ts │ │ └── User.ts │ ├── Services/ │ │ └── SpotifyService.ts │ └── Validators/ │ ├── CreateMatchValidator.ts │ └── CreateProfileValidator.ts ├── commands/ │ └── index.ts ├── compose.yml ├── config/ │ ├── ally.ts │ ├── app.ts │ ├── auth.ts │ ├── bodyparser.ts │ ├── cors.ts │ ├── database.ts │ ├── drive.ts │ └── hash.ts ├── contracts/ │ ├── ally.ts │ ├── auth.ts │ ├── drive.ts │ ├── env.ts │ ├── events.ts │ ├── hash.ts │ └── tests.ts ├── database/ │ ├── factories/ │ │ └── index.ts │ ├── migrations/ │ │ ├── 1698432448829_users.ts │ │ ├── 1698432448832_api_tokens.ts │ │ ├── 1698440823891_social_tokens.ts │ │ ├── 1698491899562_genders.ts │ │ ├── 1698491899563_profiles.ts │ │ ├── 1698528332152_artists.ts │ │ ├── 1698570713652_genres.ts │ │ ├── 1698571837102_tracks.ts │ │ └── 1698942906705_matches.ts │ └── seeders/ │ └── Gender.ts ├── env.ts ├── package.json ├── providers/ │ └── AppProvider.ts ├── server.ts ├── start/ │ ├── kernel.ts │ └── routes.ts ├── test.ts ├── tests/ │ ├── bootstrap.ts │ └── functional/ │ └── hello_world.spec.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .adonisrc.json ================================================ { "typescript": true, "commands": [ "./commands", "@adonisjs/core/build/commands/index.js", "@adonisjs/repl/build/commands", "@adonisjs/lucid/build/commands" ], "exceptionHandlerNamespace": "App/Exceptions/Handler", "aliases": { "App": "app", "Config": "config", "Database": "database", "Contracts": "contracts" }, "preloads": [ "./start/routes", "./start/kernel" ], "providers": [ "./providers/AppProvider", "@adonisjs/core", "@adonisjs/lucid", "@adonisjs/ally", "@adonisjs/auth" ], "aceProviders": [ "@adonisjs/repl" ], "tests": { "suites": [ { "name": "functional", "files": [ "tests/functional/**/*.spec(.ts|.js)" ], "timeout": 60000 } ] }, "testProviders": [ "@japa/preset-adonis/TestsProvider" ] } ================================================ FILE: .dockerignore ================================================ node_modules ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.json] insert_final_newline = false [*.md] trim_trailing_whitespace = false ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: - main env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push-image: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v3 - name: Set up QEMU uses: docker/setup-qemu-action@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to GitHub Packages uses: docker/login-action@v1 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Prepare metadata id: meta uses: docker/metadata-action@v3 with: images: ghcr.io/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=ref,event=pr type=ref,event=tag - name: Build and push uses: docker/build-push-action@v2 with: build-args: | APP_RELEASE=${{ github.sha }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - name: Deploy the new image uses: appleboy/ssh-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} key: ${{ secrets.KEY }} script: | cd ~/stacks docker stack deploy -c boumboum-back.yml boumboum-back --with-registry-auth ================================================ FILE: .gitignore ================================================ node_modules build coverage .vscode .DS_STORE .env tmp ================================================ FILE: Dockerfile ================================================ FROM node:20-alpine3.18 as base RUN apk --no-cache add curl # All deps stage FROM base as deps WORKDIR /app ADD package.json package-lock.json ./ RUN npm ci # Production only deps stage FROM base as production-deps WORKDIR /app ADD package.json package-lock.json ./ RUN npm ci --omit=dev RUN wget https://gobinaries.com/tj/node-prune --output-document - | /bin/sh && node-prune # Build stage FROM base as build WORKDIR /app COPY --from=deps /app/node_modules /app/node_modules ADD . . RUN node ace build --production --ignore-ts-errors # Production stage FROM base ENV NODE_ENV=production WORKDIR /app COPY --from=production-deps /app/node_modules /app/node_modules COPY --from=build /app/build /app EXPOSE 8080 CMD ["node", "./server.js"] ================================================ FILE: README.md ================================================ # Music Match API with AdonisJS An innovative application that connects people based on their musical preferences, drawing inspiration from Tinder. ## User Flow ### User Registration/Login - Users sign up or log in using their Spotify credentials. ### Profile Creation - Users complete their profiles, including basic information (name, date of birth, brief description, gender preference, and profile picture). - Users select and customize their top 4 favorite tracks from Spotify, retrieved using the Spotify API. ### Matchmaking - Users are presented with potential matches based on their gender preference. ### Interaction - Users see their matches. - For mutual matches, they can view the email of the match. ## API Requirements We seek a skilled Node.js developer to create this API using the AdonisJS framework. The chosen database engine can be PostgreSQL or MySQL based on your expertise and recommendation. ## Required Endpoints 1. **User Registration/Login** - Implement OAuth2 authentication with Spotify to allow users to register or log in. 2. **Logout Endpoint** - Provide a secure endpoint for users to log out of their accounts. 3. **Profile Management** - Create endpoints for users to complete their profiles, including basic information and their preferred Spotify tracks. 4. **Matching Endpoint** - Develop an endpoint to retrieve and match users based on their gender preference. 5. **Spotify Integration** - Implement an endpoint to search for songs using the Spotify API. 6. **Match History** - Develop an endpoint to list all matches, indicating whether the match is mutual. Include relevant user data for each match. ## Additional Spotify Data Upon user registration, it's crucial to save the user's Spotify preferences for future use, including: - User's top 20 artists - User's top 20 tracks - Artists the user follows ## Technical Details - **Framework:** AdonisJS - **Database:** MySQL - **Authentication:** OAuth2 with Spotify ## Getting Started 1. **Install Dependencies** ```bash npm install ``` 2. **Configure Database Connection** - Set up the database connection details in your `.env` file. 3. **Configure Spotify API** - Add your Spotify client_id and client_secret in your `.env` file. 4. **Run Migrations and Seed Data** ```bash node ace migration:run node ace db:seed ``` 5. **Start the Application** ```bash npm run dev ``` 6. **Access the API at** - [http://localhost:3333/api](http://localhost:3333/api) ## API Documentation - For the Login API (http://localhost:3333/api/signin), open it in a browser as the request will be redirected to Spotify for authentication. This API won't work in Postman. - All other APIs are provided in the Postman collection. ================================================ FILE: ace ================================================ /* |-------------------------------------------------------------------------- | Ace Commands |-------------------------------------------------------------------------- | | This file is the entry point for running ace commands. | */ require('reflect-metadata') require('source-map-support').install({ handleUncaughtExceptions: false }) const { Ignitor } = require('@adonisjs/core/build/standalone') new Ignitor(__dirname) .ace() .handle(process.argv.slice(2)) ================================================ FILE: ace-manifest.json ================================================ { "commands": { "dump:rcfile": { "settings": {}, "commandPath": "@adonisjs/core/build/commands/DumpRc", "commandName": "dump:rcfile", "description": "Dump contents of .adonisrc.json file along with defaults", "args": [], "aliases": [], "flags": [] }, "list:routes": { "settings": { "loadApp": true, "stayAlive": true }, "commandPath": "@adonisjs/core/build/commands/ListRoutes/index", "commandName": "list:routes", "description": "List application routes", "args": [], "aliases": [], "flags": [ { "name": "verbose", "propertyName": "verbose", "type": "boolean", "description": "Display more information" }, { "name": "reverse", "propertyName": "reverse", "type": "boolean", "alias": "r", "description": "Reverse routes display" }, { "name": "methods", "propertyName": "methodsFilter", "type": "array", "alias": "m", "description": "Filter routes by method" }, { "name": "patterns", "propertyName": "patternsFilter", "type": "array", "alias": "p", "description": "Filter routes by the route pattern" }, { "name": "names", "propertyName": "namesFilter", "type": "array", "alias": "n", "description": "Filter routes by route name" }, { "name": "json", "propertyName": "json", "type": "boolean", "description": "Output as JSON" }, { "name": "table", "propertyName": "table", "type": "boolean", "description": "Output as Table" }, { "name": "max-width", "propertyName": "maxWidth", "type": "number", "description": "Specify maximum rendering width. Ignored for JSON Output" } ] }, "generate:key": { "settings": {}, "commandPath": "@adonisjs/core/build/commands/GenerateKey", "commandName": "generate:key", "description": "Generate a new APP_KEY secret", "args": [], "aliases": [], "flags": [] }, "repl": { "settings": { "loadApp": true, "environment": "repl", "stayAlive": true }, "commandPath": "@adonisjs/repl/build/commands/AdonisRepl", "commandName": "repl", "description": "Start a new REPL session", "args": [], "aliases": [], "flags": [] }, "db:seed": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/DbSeed", "commandName": "db:seed", "description": "Execute database seeders", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection for the seeders", "alias": "c" }, { "name": "interactive", "propertyName": "interactive", "type": "boolean", "description": "Run seeders in interactive mode", "alias": "i" }, { "name": "files", "propertyName": "files", "type": "array", "description": "Define a custom set of seeders files names to run", "alias": "f" }, { "name": "compact-output", "propertyName": "compactOutput", "type": "boolean", "description": "A compact single-line output" } ] }, "db:wipe": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/DbWipe", "commandName": "db:wipe", "description": "Drop all tables, views and types in database", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection", "alias": "c" }, { "name": "drop-views", "propertyName": "dropViews", "type": "boolean", "description": "Drop all views" }, { "name": "drop-types", "propertyName": "dropTypes", "type": "boolean", "description": "Drop all custom types (Postgres only)" }, { "name": "force", "propertyName": "force", "type": "boolean", "description": "Explicitly force command to run in production" } ] }, "db:truncate": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/DbTruncate", "commandName": "db:truncate", "description": "Truncate all tables in database", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection", "alias": "c" }, { "name": "force", "propertyName": "force", "type": "boolean", "description": "Explicitly force command to run in production" } ] }, "make:model": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/MakeModel", "commandName": "make:model", "description": "Make a new Lucid model", "args": [ { "type": "string", "propertyName": "name", "name": "name", "required": true, "description": "Name of the model class" } ], "aliases": [], "flags": [ { "name": "migration", "propertyName": "migration", "type": "boolean", "alias": "m", "description": "Generate the migration for the model" }, { "name": "controller", "propertyName": "controller", "type": "boolean", "alias": "c", "description": "Generate the controller for the model" }, { "name": "factory", "propertyName": "factory", "type": "boolean", "alias": "f", "description": "Generate a factory for the model" } ] }, "make:migration": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/MakeMigration", "commandName": "make:migration", "description": "Make a new migration file", "args": [ { "type": "string", "propertyName": "name", "name": "name", "required": true, "description": "Name of the migration file" } ], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "The connection flag is used to lookup the directory for the migration file" }, { "name": "folder", "propertyName": "folder", "type": "string", "description": "Pre-select a migration directory" }, { "name": "create", "propertyName": "create", "type": "string", "description": "Define the table name for creating a new table" }, { "name": "table", "propertyName": "table", "type": "string", "description": "Define the table name for altering an existing table" } ] }, "make:seeder": { "settings": {}, "commandPath": "@adonisjs/lucid/build/commands/MakeSeeder", "commandName": "make:seeder", "description": "Make a new Seeder file", "args": [ { "type": "string", "propertyName": "name", "name": "name", "required": true, "description": "Name of the seeder class" } ], "aliases": [], "flags": [] }, "make:factory": { "settings": {}, "commandPath": "@adonisjs/lucid/build/commands/MakeFactory", "commandName": "make:factory", "description": "Make a new factory", "args": [ { "type": "string", "propertyName": "model", "name": "model", "required": true, "description": "The name of the model" } ], "aliases": [], "flags": [ { "name": "model-path", "propertyName": "modelPath", "type": "string", "description": "The path to the model" }, { "name": "exact", "propertyName": "exact", "type": "boolean", "description": "Create the factory with the exact name as provided", "alias": "e" } ] }, "migration:run": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/Migration/Run", "commandName": "migration:run", "description": "Migrate database by running pending migrations", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection", "alias": "c" }, { "name": "force", "propertyName": "force", "type": "boolean", "description": "Explicitly force to run migrations in production" }, { "name": "dry-run", "propertyName": "dryRun", "type": "boolean", "description": "Do not run actual queries. Instead view the SQL output" }, { "name": "compact-output", "propertyName": "compactOutput", "type": "boolean", "description": "A compact single-line output" }, { "name": "disable-locks", "propertyName": "disableLocks", "type": "boolean", "description": "Disable locks acquired to run migrations safely" } ] }, "migration:rollback": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/Migration/Rollback", "commandName": "migration:rollback", "description": "Rollback migrations to a specific batch number", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection", "alias": "c" }, { "name": "force", "propertyName": "force", "type": "boolean", "description": "Explictly force to run migrations in production" }, { "name": "dry-run", "propertyName": "dryRun", "type": "boolean", "description": "Do not run actual queries. Instead view the SQL output" }, { "name": "batch", "propertyName": "batch", "type": "number", "description": "Define custom batch number for rollback. Use 0 to rollback to initial state" }, { "name": "compact-output", "propertyName": "compactOutput", "type": "boolean", "description": "A compact single-line output" }, { "name": "disable-locks", "propertyName": "disableLocks", "type": "boolean", "description": "Disable locks acquired to run migrations safely" } ] }, "migration:status": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/Migration/Status", "commandName": "migration:status", "description": "View migrations status", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection", "alias": "c" } ] }, "migration:reset": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/Migration/Reset", "commandName": "migration:reset", "description": "Rollback all migrations", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection", "alias": "c" }, { "name": "force", "propertyName": "force", "type": "boolean", "description": "Explicitly force command to run in production" }, { "name": "dry-run", "propertyName": "dryRun", "type": "boolean", "description": "Do not run actual queries. Instead view the SQL output" }, { "name": "disable-locks", "propertyName": "disableLocks", "type": "boolean", "description": "Disable locks acquired to run migrations safely" } ] }, "migration:refresh": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/Migration/Refresh", "commandName": "migration:refresh", "description": "Rollback and migrate database", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection", "alias": "c" }, { "name": "force", "propertyName": "force", "type": "boolean", "description": "Explicitly force command to run in production" }, { "name": "dry-run", "propertyName": "dryRun", "type": "boolean", "description": "Do not run actual queries. Instead view the SQL output" }, { "name": "seed", "propertyName": "seed", "type": "boolean", "description": "Run seeders" }, { "name": "disable-locks", "propertyName": "disableLocks", "type": "boolean", "description": "Disable locks acquired to run migrations safely" } ] }, "migration:fresh": { "settings": { "loadApp": true }, "commandPath": "@adonisjs/lucid/build/commands/Migration/Fresh", "commandName": "migration:fresh", "description": "Drop all tables and re-migrate the database", "args": [], "aliases": [], "flags": [ { "name": "connection", "propertyName": "connection", "type": "string", "description": "Define a custom database connection", "alias": "c" }, { "name": "force", "propertyName": "force", "type": "boolean", "description": "Explicitly force command to run in production" }, { "name": "seed", "propertyName": "seed", "type": "boolean", "description": "Run seeders" }, { "name": "drop-views", "propertyName": "dropViews", "type": "boolean", "description": "Drop all views" }, { "name": "drop-types", "propertyName": "dropTypes", "type": "boolean", "description": "Drop all custom types (Postgres only)" }, { "name": "disable-locks", "propertyName": "disableLocks", "type": "boolean", "description": "Disable locks acquired to run migrations safely" } ] } }, "aliases": {} } ================================================ FILE: app/Controllers/Http/GendersController.ts ================================================ import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; import Gender from "App/Models/Gender"; export default class GendersController { public async index({ response }: HttpContextContract) { try { const genders = await Gender.query(); return response.json({ status: true, data: genders, message: "Successfully fetched genders", }); } catch (err) { return response.json({ status: false, message: "Something went wrong.", }); } } } ================================================ FILE: app/Controllers/Http/MatchesController.ts ================================================ import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; import { schema } from '@ioc:Adonis/Core/Validator' import User from "App/Models/User"; import Match from "App/Models/Match"; import CreateMatchValidator from "App/Validators/CreateMatchValidator"; export default class MatchesController { //retrive list of user's based on there gender preference public async get({ response, auth }: HttpContextContract) { try { const userId = auth.user?.id; if (!userId) { return response.json({ message: "kindly login", }); } const currentUser = await User.query() .where("id", userId) .preload("profile") .first(); const profile = currentUser?.profile; if (!profile?.preferedGenderId) return response.json("Profile not exist"); const users = await User.query() .whereNot("id", userId) .preload("profile") .with("profile", (q) => { q.where("prefered_gender_id", profile.preferedGenderId); }); const mappedUsers = users?.map((u) => { return { id: u.id, name: u.name, avatar: u?.profile?.avatar, }; }); return response.json({ data: mappedUsers, }); } catch (err) { console.log("errrrr", err); } } public async mutualMatch({ request, response, auth }: HttpContextContract) { try { const authId = auth.user?.id; if (!authId) { response.status(401); return; } const payload = await request.validate(CreateMatchValidator); const { userId } = payload; if(authId == userId) { return response.json({ message: "Cannot mark youself as match." }) } const userExist = await User.query().where("id", userId).first(); if(!userExist) { return response.json({ message: "User not found." }) } const matchExist = await Match.query() .where("matcher_user_id", userId) .where("matched_user_id", authId) .first(); if (matchExist) { await Match.query().where("id", matchExist.id).update({ mutual_match: 1, match_date: new Date(), }); const matchedUser = await User.query() .where("id", userId) .select("name", "email") // .preload("profile") .first(); const userData = { name: matchedUser?.name, email: matchedUser?.email, }; return response.json({ message: "It's a mutual match", data: userData, }); } const newMatch = new Match(); newMatch.matcherUserId = authId; newMatch.matchedUserId = userId; await newMatch.save(); return response.json({ message: "Match has been marked.", }); } catch (err) { return response.json({ message: "Something went wrong.", errors: err?.messages?.errors, }); } } public async history({ response, auth }: HttpContextContract) { try { const authId = auth.user?.id; if (!authId) { return response.json({ message: "kindly login", }); } const matchHistory = await Match.query() .where("matcher_user_id", authId) .orWhere("matched_user_id", authId) .where("mutual_match", 1); const userIds = matchHistory?.map((history: Match) => { return history.matcherUserId == authId ? String(history.matchedUserId) : String(history.matcherUserId); }); const users = await User.query() .whereIn("id", userIds) .select("name", "email") return response.json({ data: users, message: "Mutual match history.", }); } catch (err) { return response.json({ message: "Something went wrong.", }); } } } ================================================ FILE: app/Controllers/Http/ProfilesController.ts ================================================ import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; import Profile from "App/Models/Profile"; import SpotifyService from "App/Services/SpotifyService"; import CreateProfileValidator from "App/Validators/CreateProfileValidator"; export default class ProfilesController { public async get({ response, auth }: HttpContextContract) { try { const userId = auth.user?.id; if (!userId) { response.status(401); return; } const profile = await Profile.query().where("user_id", userId).first(); return response.json({ data: profile, }); } catch (err) { return response.json({ status: false, message: "Something went wrong.", }); } } public async store({ request, response, auth }: HttpContextContract) { try { const userId = auth.user?.id; if (!userId) { response.status(401); return; } const payload = await request.validate(CreateProfileValidator); const { dateOfBirth, description, preferedGenderId, trackIds } = payload; const fileName = `${new Date().getTime()}.${payload.avatar.subtype}`; await payload.avatar.moveToDisk("./", { name: fileName, }); //save top 4 selected tracks by user const favoriteTracks = await SpotifyService.updateFavorityTrack( userId, trackIds ); if (!favoriteTracks?.status) throw Error("unable to update favorite tracks"); //Profile saved let profile; profile = await Profile.query().where("user_id", userId).first(); profile = profile ? profile : new Profile(); profile.dateOfBirth = new Date(dateOfBirth); profile.description = description; profile.avatar = `/uploads/${fileName}`; profile.preferedGenderId = preferedGenderId; profile.userId = userId; await profile.save(); return response.json({ status: true, message: "Profile Successfully Created", data: profile, }); } catch (err) { return response.json({ status: false, message: "Unable to create profile", errors: err?.messages?.errors, }); } } } ================================================ FILE: app/Controllers/Http/SpotifyController.ts ================================================ import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; import SpotifyService from "App/Services/SpotifyService"; export default class SpotifiesController { public async artists({ response, auth }: HttpContextContract) { try { const userId = auth.user?.id; const topArtists = await SpotifyService.getArtists(userId); const savedArtists = await SpotifyService.saveArtists(userId, topArtists); return response.json(savedArtists); } catch (err) { console.log("err", err); } } public async tracks({ response, auth }: HttpContextContract) { try { const userId = auth.user?.id; const topTracks = await SpotifyService.getTracks(userId); const mappdTracks = topTracks?.map((track) => { return { // image: track?.preview_url, // uri: track.uri, popularity: track.popularity, name: track.name, trackId: track.id, album: track?.albun?.name, // artists: artists, }; }); // const savedTracks = await SpotifyService.saveTracks(userId, topTracks); return response.json(mappdTracks); } catch (err) { console.log("err", err); } } public async trackByName({request, response, auth}: HttpContextContract) { try { const userId = auth.user?.id; const { name } = request.qs() const tracks = await SpotifyService.getTracksByName(userId, name) const mappedTracks = tracks?.map((track) => { return { uri: track.uri, popularity: track.popularity, name: track.name, trackId: track.id, album: track?.album?.name, } }) return response.json({ status: true, data: mappedTracks }) } catch(err) { return response.json({ status: false, message: "Error fetching track's" }) } } } ================================================ FILE: app/Controllers/Http/UsersController.ts ================================================ import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; import Artist from "App/Models/Artist"; import SocialToken from "App/Models/SocialToken"; import Track from "App/Models/Track"; import User from "App/Models/User"; import SpotifyService from "App/Services/SpotifyService"; export default class UsersController { public async redirect({ ally }: HttpContextContract) { return ally.use("spotify").stateless().redirect(); } public async handleCallback({ ally, auth, response }: HttpContextContract) { try { const spotify = ally.use("spotify").stateless(); /** * User has explicitly denied the login request */ if (spotify.accessDenied()) { return "Access was denied"; } /** * Unable to verify the CSRF state */ if (spotify.stateMisMatch()) { return "Request expired. try again"; } /** * There was an unknown error during the redirect */ if (spotify.hasError()) { return spotify.getError(); } /** * Managing error states here */ const user = await spotify.user(); const { token } = user; const findUser = { email: user.email as string, }; const userDetails = { name: user.name as string, email: user.email as string, provider: "spotify", access_token: token.token as any, }; const newUser = await User.firstOrCreate(findUser, userDetails); if (!newUser) { return response.json({ status: false, message: "Something went wrong.", }); } /* Save Social Token */ let socialToken = await SocialToken.query() .where("user_id", newUser.id) .first(); socialToken = socialToken ? socialToken : new SocialToken(); socialToken.user_id = newUser.id; socialToken.token = token.token; socialToken.refreshToken = token.refreshToken; socialToken.type = token.type; socialToken.expiresAt = token.expiresAt?.toString(); await socialToken.save(); /* Save Social Token */ //save top 20 tracks from spotify service const trackExist = await Track.query().where("user_id", newUser.id); if (!trackExist?.length) { const topTracks = await SpotifyService.getTracks(newUser.id); const topTracksSaved = await SpotifyService.saveTracks( newUser.id, topTracks ); if (!topTracksSaved?.status) throw Error("Unable to save top tracks"); } //save top 20 artists from spotify service const artists = await Artist.query().where("user_id", newUser.id); if (!artists?.length) { const topArtists = await SpotifyService.getArtists(newUser.id); const topArtistsSaved = await SpotifyService.saveArtists( newUser.id, topArtists ); if (!topArtistsSaved?.status) throw Error("Unable to save artists tracks"); } // Generate API token const userToken = await auth.use("api").generate(newUser, { expiresIn: "90 mins", }); response.json({ /* newUser, */ userToken /* , socialToken */ }); } catch (err) { response.json({ status: false, message: "Something went wrong.", }); } } public async logout({ auth, response }: HttpContextContract) { await auth.use("api").revoke(); return response.json({ revoked: true, }); } } ================================================ FILE: app/Exceptions/Handler.ts ================================================ /* |-------------------------------------------------------------------------- | Http Exception Handler |-------------------------------------------------------------------------- | | AdonisJs will forward all exceptions occurred during an HTTP request to | the following class. You can learn more about exception handling by | reading docs. | | The exception handler extends a base `HttpExceptionHandler` which is not | mandatory, however it can do lot of heavy lifting to handle the errors | properly. | */ import Logger from '@ioc:Adonis/Core/Logger' import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler' export default class ExceptionHandler extends HttpExceptionHandler { constructor () { super(Logger) } } ================================================ FILE: app/Middleware/Auth.ts ================================================ import { AuthenticationException } from '@adonisjs/auth/build/standalone' import type { GuardsList } from '@ioc:Adonis/Addons/Auth' import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' /** * Auth middleware is meant to restrict un-authenticated access to a given route * or a group of routes. * * You must register this middleware inside `start/kernel.ts` file under the list * of named middleware. */ export default class AuthMiddleware { /** * The URL to redirect to when request is Unauthorized */ protected redirectTo = '/login' /** * Authenticates the current HTTP request against a custom set of defined * guards. * * The authentication loop stops as soon as the user is authenticated using any * of the mentioned guards and that guard will be used by the rest of the code * during the current request. */ protected async authenticate(auth: HttpContextContract['auth'], guards: (keyof GuardsList)[]) { /** * Hold reference to the guard last attempted within the for loop. We pass * the reference of the guard to the "AuthenticationException", so that * it can decide the correct response behavior based upon the guard * driver */ let guardLastAttempted: string | undefined for (let guard of guards) { guardLastAttempted = guard if (await auth.use(guard).check()) { /** * Instruct auth to use the given guard as the default guard for * the rest of the request, since the user authenticated * succeeded here */ auth.defaultGuard = guard return true } } /** * Unable to authenticate using any guard */ throw new AuthenticationException( 'Unauthorized access', 'E_UNAUTHORIZED_ACCESS', guardLastAttempted, this.redirectTo, ) } /** * Handle request */ public async handle ( { auth }: HttpContextContract, next: () => Promise, customGuards: (keyof GuardsList)[] ) { /** * Uses the user defined guards or the default guard mentioned in * the config file */ const guards = customGuards.length ? customGuards : [auth.name] await this.authenticate(auth, guards) await next() } } ================================================ FILE: app/Middleware/SilentAuth.ts ================================================ import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' /** * Silent auth middleware can be used as a global middleware to silent check * if the user is logged-in or not. * * The request continues as usual, even when the user is not logged-in. */ export default class SilentAuthMiddleware { /** * Handle request */ public async handle({ auth }: HttpContextContract, next: () => Promise) { /** * Check if user is logged-in or not. If yes, then `ctx.auth.user` will be * set to the instance of the currently logged in user. */ await auth.check() await next() } } ================================================ FILE: app/Models/ApiToken.ts ================================================ import { DateTime } from 'luxon' import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' export default class ApiToken extends BaseModel { @column({ isPrimary: true }) public id: number @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime @column() public token: String } ================================================ FILE: app/Models/Artist.ts ================================================ import { DateTime } from 'luxon' import { BaseModel, HasMany, column, hasMany } from '@ioc:Adonis/Lucid/Orm' import Genre from './Genre' export default class Artist extends BaseModel { @column({ isPrimary: true }) public id: number @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime @column() public userId: String @column() public name: String @column() public type: String @column() public popularity: String @column() public uri: String @column() public spotifyArtistId: String @column() public artistImage: String @hasMany(() => Genre) public genres: HasMany } ================================================ FILE: app/Models/Gender.ts ================================================ import { DateTime } from 'luxon' import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' export default class Gender extends BaseModel { @column({ isPrimary: true }) public id: number @column() public name: string @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime } ================================================ FILE: app/Models/Genre.ts ================================================ import { DateTime } from 'luxon' import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' export default class Genre extends BaseModel { @column({ isPrimary: true }) public id: number @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime @column() public name: String @column() public artistId: Number } ================================================ FILE: app/Models/Match.ts ================================================ import { DateTime } from 'luxon' import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' export default class Match extends BaseModel { @column({ isPrimary: true }) public id: number @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime @column() public matcherUserId: Number @column() public matchedUserId: Number @column() public mutualMatch: Boolean @column() public matchDate: Date } ================================================ FILE: app/Models/Profile.ts ================================================ import { DateTime } from 'luxon' import { BaseModel, HasOne, column, hasOne } from '@ioc:Adonis/Lucid/Orm' import Gender from './Gender' export default class Profile extends BaseModel { @column({ isPrimary: true }) public id: number @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime @column() public dateOfBirth: Date @column() public description: String @column() public avatar: String @column() public preferedGenderId: Number @column() public userId: Number @hasOne(() => Gender) public preferedGender: HasOne } ================================================ FILE: app/Models/SocialToken.ts ================================================ import { DateTime } from 'luxon' import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm' export default class SocialToken extends BaseModel { @column({ isPrimary: true }) public id: number @column() public user_id: number @column() public token: string @column() public type: string @column() public refreshToken: string @column() public expiresAt: String @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime } ================================================ FILE: app/Models/Track.ts ================================================ import { DateTime } from 'luxon' import { BaseModel, HasMany, column, hasMany } from '@ioc:Adonis/Lucid/Orm' import Artist from './Artist' export default class Track extends BaseModel { @column({ isPrimary: true }) public id: number @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime @column() public userId: String @column() public uri: String @column() public popularity: String @column() public name: String @column() public tractImage: String @column() public trackId: String @column() public album: String @column() public favorite: Number @hasMany(() => Artist) public artists: HasMany } ================================================ FILE: app/Models/User.ts ================================================ import { DateTime } from 'luxon' import Hash from '@ioc:Adonis/Core/Hash' import { column, beforeSave, BaseModel, hasMany, HasMany, hasOne, HasOne } from '@ioc:Adonis/Lucid/Orm' import Track from './Track' import Artist from './Artist' import Profile from './Profile' export default class User extends BaseModel { @column({ isPrimary: true }) public id: number @column() public name: string @column() public provider: string @column() public access_token: string @column() public email: string @column({ serializeAs: null }) public password: string @column() public rememberMeToken: string | null @column.dateTime({ autoCreate: true }) public createdAt: DateTime @column.dateTime({ autoCreate: true, autoUpdate: true }) public updatedAt: DateTime // @beforeSave() // public static async hashPassword (user: User) { // if (user.$dirty.password) { // user.password = await Hash.make(user.password) // } // } @hasOne(() => Profile) public profile: HasOne @hasMany(() => Track) public tracks: HasMany @hasMany(() => Artist) public artists: HasMany } ================================================ FILE: app/Services/SpotifyService.ts ================================================ import Env from "@ioc:Adonis/Core/Env"; import Artist from "App/Models/Artist"; import Genre from "App/Models/Genre"; import SocialToken from "App/Models/SocialToken"; import Track from "App/Models/Track"; import Axios from "axios"; export default class SpotifyService { public static async getArtists(userId) { const SPOTIFY_URL = Env.get("SPOTIFY_URL"); const social = await SocialToken.query().where("user_id", userId).first(); const resp = await Axios.get(`${SPOTIFY_URL}/me/top/artists`, { headers: { Authorization: `Bearer ${social?.token}`, }, }); return resp?.data?.items; } public static async getTracks(userId) { const SPOTIFY_URL = Env.get("SPOTIFY_URL"); const social = await SocialToken.query().where("user_id", userId).first(); const resp = await Axios.get( `${SPOTIFY_URL}/me/top/tracks?time_range=medium_term&limit=5`, { headers: { Authorization: `Bearer ${social?.token}`, }, } ); return resp?.data?.items; } public static async saveArtists(userId, artists) { try { for (let artist of artists) { //store artists const newArtist = new Artist(); newArtist.userId = userId; newArtist.name = artist?.name; newArtist.type = artist?.type; newArtist.popularity = artist?.popularity; newArtist.uri = artist?.uri; newArtist.spotifyArtistId = artist?.id; newArtist.artistImage = artist?.images ? artist?.images[0]?.url : null; artist.genres = artist.genres?.map((name) => { let genre = new Genre(); genre.name = name; return genre; }); //store artists genres await newArtist.related("genres").saveMany(artist.genres); } return { status: true, }; } catch (err) { console.log("FAILIURE"); console.log("err", err); return { status: false, }; } } public static async saveTracks(userId, tracks) { try { for (let track of tracks) { //store tracks const newTrack = new Track(); newTrack.userId = userId; newTrack.uri = track.uri; newTrack.popularity = track.popularity; newTrack.name = track.name; newTrack.trackId = track.id; newTrack.album = track?.album?.name; await newTrack.save(); } return { status: true, }; } catch (err) { return { status: false, }; } } public static async getTracksByIds(userId, trackIds) { const SPOTIFY_URL = Env.get("SPOTIFY_URL"); const commaSeparatedIds = trackIds.join(","); const social = await SocialToken.query().where("user_id", userId).first(); const resp = await Axios.get( `${SPOTIFY_URL}/tracks?ids=${commaSeparatedIds}`, { headers: { Authorization: `Bearer ${social?.token}`, }, } ); return resp?.data?.tracks; } public static getTracksData(tracks) { const mappdTracks = tracks?.map((track) => { return { popularity: track.popularity, name: track.name, trackId: track.id, album: track?.albun?.name, }; }); return mappdTracks; } public static async updateFavorityTrack(userId, trackIds) { try { const markFavorite = await Track.query() .where("user_id", userId) .whereIn("track_id", trackIds) .update({ favorite: 1, }); return { status: true, data: markFavorite, }; } catch (err) { return { status: false, }; } } public static async getTracksByName(userId, name) { const SPOTIFY_URL = Env.get("SPOTIFY_URL"); const social = await SocialToken.query().where("user_id", userId).first(); const resp = await Axios.get( `${SPOTIFY_URL}/search?q=track:${name}&type=track`, { headers: { Authorization: `Bearer ${social?.token}`, }, } ); return resp?.data?.tracks?.items; } } ================================================ FILE: app/Validators/CreateMatchValidator.ts ================================================ import { schema } from '@ioc:Adonis/Core/Validator' import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class CreateMatchValidator { constructor(protected ctx: HttpContextContract) {} /* * Define schema to validate the "shape", "type", "formatting" and "integrity" of data. * * For example: * 1. The username must be of data type string. But then also, it should * not contain special characters or numbers. * ``` * schema.string({}, [ rules.alpha() ]) * ``` * * 2. The email must be of data type string, formatted as a valid * email. But also, not used by any other user. * ``` * schema.string({}, [ * rules.email(), * rules.unique({ table: 'users', column: 'email' }), * ]) * ``` */ public schema = schema.create({ userId: schema.number(), }) /** * Custom messages for validation failures. You can make use of dot notation `(.)` * for targeting nested fields and array expressions `(*)` for targeting all * children of an array. For example: * * { * 'profile.username.required': 'Username is required', * 'scores.*.number': 'Define scores as valid numbers' * } * */ public messages = { required: 'The {{ field }} is required.', // 'username.unique': 'Username not available' } } ================================================ FILE: app/Validators/CreateProfileValidator.ts ================================================ import { schema } from '@ioc:Adonis/Core/Validator' import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class CreateProfileValidator { constructor(protected ctx: HttpContextContract) {} /* * Define schema to validate the "shape", "type", "formatting" and "integrity" of data. * * For example: * 1. The username must be of data type string. But then also, it should * not contain special characters or numbers. * ``` * schema.string({}, [ rules.alpha() ]) * ``` * * 2. The email must be of data type string, formatted as a valid * email. But also, not used by any other user. * ``` * schema.string({}, [ * rules.email(), * rules.unique({ table: 'users', column: 'email' }), * ]) * ``` */ public schema = schema.create({ dateOfBirth: schema.string(), description: schema.string.nullableAndOptional(), preferedGenderId: schema.number(), trackIds: schema.array().members(schema.string()), avatar: schema.file({ size: "5mb", extnames: ["jpg", "png", "jpeg"], }), }) /** * Custom messages for validation failures. You can make use of dot notation `(.)` * for targeting nested fields and array expressions `(*)` for targeting all * children of an array. For example: * * { * 'profile.username.required': 'Username is required', * 'scores.*.number': 'Define scores as valid numbers' * } * */ public messages = { required: 'The {{ field }} is required.', // 'username.unique': 'Username not available' } } ================================================ FILE: commands/index.ts ================================================ import { listDirectoryFiles } from '@adonisjs/core/build/standalone' import Application from '@ioc:Adonis/Core/Application' /* |-------------------------------------------------------------------------- | Exporting an array of commands |-------------------------------------------------------------------------- | | Instead of manually exporting each file from this directory, we use the | helper `listDirectoryFiles` to recursively collect and export an array | of filenames. | | Couple of things to note: | | 1. The file path must be relative from the project root and not this directory. | 2. We must ignore this file to avoid getting into an infinite loop | */ export default listDirectoryFiles(__dirname, Application.appRoot, ['./commands/index']) ================================================ FILE: compose.yml ================================================ services: db: image: mysql:8.0 platform: linux/x86_64 ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD= - MYSQL_ALLOW_EMPTY_PASSWORD=true volumes: mysql-data: ================================================ FILE: config/ally.ts ================================================ /** * Config source: https://git.io/JOdi5 * * Feel free to let us know via PR, if you find something broken in this config * file. */ import Env from '@ioc:Adonis/Core/Env' import { AllyConfig } from '@ioc:Adonis/Addons/Ally' /* |-------------------------------------------------------------------------- | Ally Config |-------------------------------------------------------------------------- | | The `AllyConfig` relies on the `SocialProviders` interface which is | defined inside `contracts/ally.ts` file. | */ const allyConfig: AllyConfig = { /* |-------------------------------------------------------------------------- | Spotify driver |-------------------------------------------------------------------------- */ spotify: { driver: 'spotify', clientId: Env.get('SPOTIFY_CLIENT_ID'), clientSecret: Env.get('SPOTIFY_CLIENT_SECRET'), callbackUrl: Env.get('SPOTIFY_CALLBACK_URL', 'http://localhost:3333/api/signin-callback'), scopes: ['user-read-email', 'user-top-read', 'user-follow-read'], showDialog: false }, } export default allyConfig ================================================ FILE: config/app.ts ================================================ /** * Config source: https://git.io/JfefZ * * Feel free to let us know via PR, if you find something broken in this config * file. */ import proxyAddr from 'proxy-addr' import Env from '@ioc:Adonis/Core/Env' import type { ServerConfig } from '@ioc:Adonis/Core/Server' import type { LoggerConfig } from '@ioc:Adonis/Core/Logger' import type { ProfilerConfig } from '@ioc:Adonis/Core/Profiler' import type { ValidatorConfig } from '@ioc:Adonis/Core/Validator' /* |-------------------------------------------------------------------------- | Application secret key |-------------------------------------------------------------------------- | | The secret to encrypt and sign different values in your application. | Make sure to keep the `APP_KEY` as an environment variable and secure. | | Note: Changing the application key for an existing app will make all | the cookies invalid and also the existing encrypted data will not | be decrypted. | */ export const appKey: string = Env.get('APP_KEY') /* |-------------------------------------------------------------------------- | Http server configuration |-------------------------------------------------------------------------- | | The configuration for the HTTP(s) server. Make sure to go through all | the config properties to make keep server secure. | */ export const http: ServerConfig = { /* |-------------------------------------------------------------------------- | Allow method spoofing |-------------------------------------------------------------------------- | | Method spoofing enables defining custom HTTP methods using a query string | `_method`. This is usually required when you are making traditional | form requests and wants to use HTTP verbs like `PUT`, `DELETE` and | so on. | */ allowMethodSpoofing: false, /* |-------------------------------------------------------------------------- | Subdomain offset |-------------------------------------------------------------------------- */ subdomainOffset: 2, /* |-------------------------------------------------------------------------- | Request Ids |-------------------------------------------------------------------------- | | Setting this value to `true` will generate a unique request id for each | HTTP request and set it as `x-request-id` header. | */ generateRequestId: false, /* |-------------------------------------------------------------------------- | Trusting proxy servers |-------------------------------------------------------------------------- | | Define the proxy servers that AdonisJs must trust for reading `X-Forwarded` | headers. | */ trustProxy: proxyAddr.compile('loopback'), /* |-------------------------------------------------------------------------- | Generating Etag |-------------------------------------------------------------------------- | | Whether or not to generate an etag for every response. | */ etag: false, /* |-------------------------------------------------------------------------- | JSONP Callback |-------------------------------------------------------------------------- */ jsonpCallbackName: 'callback', /* |-------------------------------------------------------------------------- | Cookie settings |-------------------------------------------------------------------------- */ cookie: { domain: '', path: '/', maxAge: '2h', httpOnly: true, secure: false, sameSite: false, }, /* |-------------------------------------------------------------------------- | Force Content Negotiation |-------------------------------------------------------------------------- | | The internals of the framework relies on the content negotiation to | detect the best possible response type for a given HTTP request. | | However, it is a very common these days that API servers always wants to | make response in JSON regardless of the existence of the `Accept` header. | | By setting `forceContentNegotiationTo = 'application/json'`, you negotiate | with the server in advance to always return JSON without relying on the | client to set the header explicitly. | */ forceContentNegotiationTo: 'application/json', } /* |-------------------------------------------------------------------------- | Logger |-------------------------------------------------------------------------- */ export const logger: LoggerConfig = { /* |-------------------------------------------------------------------------- | Application name |-------------------------------------------------------------------------- | | The name of the application you want to add to the log. It is recommended | to always have app name in every log line. | | The `APP_NAME` environment variable is automatically set by AdonisJS by | reading the `name` property from the `package.json` file. | */ name: Env.get('APP_NAME'), /* |-------------------------------------------------------------------------- | Toggle logger |-------------------------------------------------------------------------- | | Enable or disable logger application wide | */ enabled: true, /* |-------------------------------------------------------------------------- | Logging level |-------------------------------------------------------------------------- | | The level from which you want the logger to flush logs. It is recommended | to make use of the environment variable, so that you can define log levels | at deployment level and not code level. | */ level: Env.get('LOG_LEVEL', 'info'), /* |-------------------------------------------------------------------------- | Pretty print |-------------------------------------------------------------------------- | | It is highly advised NOT to use `prettyPrint` in production, since it | can have huge impact on performance. | */ prettyPrint: Env.get('NODE_ENV') === 'development', } /* |-------------------------------------------------------------------------- | Profiler |-------------------------------------------------------------------------- */ export const profiler: ProfilerConfig = { /* |-------------------------------------------------------------------------- | Toggle profiler |-------------------------------------------------------------------------- | | Enable or disable profiler | */ enabled: true, /* |-------------------------------------------------------------------------- | Blacklist actions/row labels |-------------------------------------------------------------------------- | | Define an array of actions or row labels that you want to disable from | getting profiled. | */ blacklist: [], /* |-------------------------------------------------------------------------- | Whitelist actions/row labels |-------------------------------------------------------------------------- | | Define an array of actions or row labels that you want to whitelist for | the profiler. When whitelist is defined, then `blacklist` is ignored. | */ whitelist: [], } /* |-------------------------------------------------------------------------- | Validator |-------------------------------------------------------------------------- | | Configure the global configuration for the validator. Here's the reference | to the default config https://git.io/JT0WE | */ export const validator: ValidatorConfig = { } ================================================ FILE: config/auth.ts ================================================ /** * Config source: https://git.io/JY0mp * * Feel free to let us know via PR, if you find something broken in this config * file. */ import type { AuthConfig } from '@ioc:Adonis/Addons/Auth' /* |-------------------------------------------------------------------------- | Authentication Mapping |-------------------------------------------------------------------------- | | List of available authentication mapping. You must first define them | inside the `contracts/auth.ts` file before mentioning them here. | */ const authConfig: AuthConfig = { guard: 'api', guards: { /* |-------------------------------------------------------------------------- | OAT Guard |-------------------------------------------------------------------------- | | OAT (Opaque access tokens) guard uses database backed tokens to authenticate | HTTP request. This guard DOES NOT rely on sessions or cookies and uses | Authorization header value for authentication. | | Use this guard to authenticate mobile apps or web clients that cannot rely | on cookies/sessions. | */ api: { driver: 'oat', /* |-------------------------------------------------------------------------- | Tokens provider |-------------------------------------------------------------------------- | | Uses SQL database for managing tokens. Use the "database" driver, when | tokens are the secondary mode of authentication. | For example: The Github personal tokens | | The foreignKey column is used to make the relationship between the user | and the token. You are free to use any column name here. | */ tokenProvider: { type: 'api', driver: 'database', table: 'api_tokens', foreignKey: 'user_id', }, provider: { /* |-------------------------------------------------------------------------- | Driver |-------------------------------------------------------------------------- | | Name of the driver | */ driver: 'lucid', /* |-------------------------------------------------------------------------- | Identifier key |-------------------------------------------------------------------------- | | The identifier key is the unique key on the model. In most cases specifying | the primary key is the right choice. | */ identifierKey: 'id', /* |-------------------------------------------------------------------------- | Uids |-------------------------------------------------------------------------- | | Uids are used to search a user against one of the mentioned columns. During | login, the auth module will search the user mentioned value against one | of the mentioned columns to find their user record. | */ uids: ['email'], /* |-------------------------------------------------------------------------- | Model |-------------------------------------------------------------------------- | | The model to use for fetching or finding users. The model is imported | lazily since the config files are read way earlier in the lifecycle | of booting the app and the models may not be in a usable state at | that time. | */ model: () => import('App/Models/User'), }, }, }, } export default authConfig ================================================ FILE: config/bodyparser.ts ================================================ /** * Config source: https://git.io/Jfefn * * Feel free to let us know via PR, if you find something broken in this config * file. */ import type { BodyParserConfig } from '@ioc:Adonis/Core/BodyParser' const bodyParserConfig: BodyParserConfig = { /* |-------------------------------------------------------------------------- | White listed methods |-------------------------------------------------------------------------- | | HTTP methods for which body parsing must be performed. It is a good practice | to avoid body parsing for `GET` requests. | */ whitelistedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'], /* |-------------------------------------------------------------------------- | JSON parser settings |-------------------------------------------------------------------------- | | The settings for the JSON parser. The types defines the request content | types which gets processed by the JSON parser. | */ json: { encoding: 'utf-8', limit: '1mb', strict: true, types: [ 'application/json', 'application/json-patch+json', 'application/vnd.api+json', 'application/csp-report', ], }, /* |-------------------------------------------------------------------------- | Form parser settings |-------------------------------------------------------------------------- | | The settings for the `application/x-www-form-urlencoded` parser. The types | defines the request content types which gets processed by the form parser. | */ form: { encoding: 'utf-8', limit: '1mb', queryString: {}, /* |-------------------------------------------------------------------------- | Convert empty strings to null |-------------------------------------------------------------------------- | | Convert empty form fields to null. HTML forms results in field string | value when the field is left blank. This option normalizes all the blank | field values to "null" | */ convertEmptyStringsToNull: true, types: [ 'application/x-www-form-urlencoded', ], }, /* |-------------------------------------------------------------------------- | Raw body parser settings |-------------------------------------------------------------------------- | | Raw body just reads the request body stream as a plain text, which you | can process by hand. This must be used when request body type is not | supported by the body parser. | */ raw: { encoding: 'utf-8', limit: '1mb', queryString: {}, types: [ 'text/*', ], }, /* |-------------------------------------------------------------------------- | Multipart parser settings |-------------------------------------------------------------------------- | | The settings for the `multipart/form-data` parser. The types defines the | request content types which gets processed by the form parser. | */ multipart: { /* |-------------------------------------------------------------------------- | Auto process |-------------------------------------------------------------------------- | | The auto process option will process uploaded files and writes them to | the `tmp` folder. You can turn it off and then manually use the stream | to pipe stream to a different destination. | | It is recommended to keep `autoProcess=true`. Unless you are processing bigger | file sizes. | */ autoProcess: true, /* |-------------------------------------------------------------------------- | Files to be processed manually |-------------------------------------------------------------------------- | | You can turn off `autoProcess` for certain routes by defining | routes inside the following array. | | NOTE: Make sure the route pattern starts with a leading slash. | | Correct | ```js | /projects/:id/file | ``` | | Incorrect | ```js | projects/:id/file | ``` */ processManually: [], /* |-------------------------------------------------------------------------- | Temporary file name |-------------------------------------------------------------------------- | | When auto processing is on. We will use this method to compute the temporary | file name. AdonisJs will compute a unique `tmpPath` for you automatically, | However, you can also define your own custom method. | */ // tmpFileName () { // }, /* |-------------------------------------------------------------------------- | Encoding |-------------------------------------------------------------------------- | | Request body encoding | */ encoding: 'utf-8', /* |-------------------------------------------------------------------------- | Convert empty strings to null |-------------------------------------------------------------------------- | | Convert empty form fields to null. HTML forms results in field string | value when the field is left blank. This option normalizes all the blank | field values to "null" | */ convertEmptyStringsToNull: true, /* |-------------------------------------------------------------------------- | Max Fields |-------------------------------------------------------------------------- | | The maximum number of fields allowed in the request body. The field includes | text inputs and files both. | */ maxFields: 1000, /* |-------------------------------------------------------------------------- | Request body limit |-------------------------------------------------------------------------- | | The total limit to the multipart body. This includes all request files | and fields data. | */ limit: '20mb', /* |-------------------------------------------------------------------------- | Types |-------------------------------------------------------------------------- | | The types that will be considered and parsed as multipart body. | */ types: [ 'multipart/form-data', ], }, } export default bodyParserConfig ================================================ FILE: config/cors.ts ================================================ /** * Config source: https://git.io/JfefC * * Feel free to let us know via PR, if you find something broken in this config * file. */ import type { CorsConfig } from '@ioc:Adonis/Core/Cors' const corsConfig: CorsConfig = { /* |-------------------------------------------------------------------------- | Enabled |-------------------------------------------------------------------------- | | A boolean to enable or disable CORS integration from your AdonisJs | application. | | Setting the value to `true` will enable the CORS for all HTTP request. However, | you can define a function to enable/disable it on per request basis as well. | */ enabled: false, // You can also use a function that return true or false. // enabled: (request) => request.url().startsWith('/api') /* |-------------------------------------------------------------------------- | Origin |-------------------------------------------------------------------------- | | Set a list of origins to be allowed for `Access-Control-Allow-Origin`. | The value can be one of the following: | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin | | Boolean (true) - Allow current request origin. | Boolean (false) - Disallow all. | String - Comma separated list of allowed origins. | Array - An array of allowed origins. | String (*) - A wildcard (*) to allow all request origins. | Function - Receives the current origin string and should return | one of the above values. | */ origin: true, /* |-------------------------------------------------------------------------- | Methods |-------------------------------------------------------------------------- | | An array of allowed HTTP methods for CORS. The `Access-Control-Request-Method` | is checked against the following list. | | Following is the list of default methods. Feel free to add more. */ methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], /* |-------------------------------------------------------------------------- | Headers |-------------------------------------------------------------------------- | | List of headers to be allowed for `Access-Control-Allow-Headers` header. | The value can be one of the following: | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers | | Boolean(true) - Allow all headers mentioned in `Access-Control-Request-Headers`. | Boolean(false) - Disallow all headers. | String - Comma separated list of allowed headers. | Array - An array of allowed headers. | Function - Receives the current header and should return one of the above values. | */ headers: true, /* |-------------------------------------------------------------------------- | Expose Headers |-------------------------------------------------------------------------- | | A list of headers to be exposed by setting `Access-Control-Expose-Headers`. | header. By default following 6 simple response headers are exposed. | | Cache-Control | Content-Language | Content-Type | Expires | Last-Modified | Pragma | | In order to add more headers, simply define them inside the following array. | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers | */ exposeHeaders: [ 'cache-control', 'content-language', 'content-type', 'expires', 'last-modified', 'pragma', ], /* |-------------------------------------------------------------------------- | Credentials |-------------------------------------------------------------------------- | | Toggle `Access-Control-Allow-Credentials` header. If value is set to `true`, | then header will be set, otherwise not. | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials | */ credentials: true, /* |-------------------------------------------------------------------------- | MaxAge |-------------------------------------------------------------------------- | | Define `Access-Control-Max-Age` header in seconds. | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age | */ maxAge: 90, } export default corsConfig ================================================ FILE: config/database.ts ================================================ /** * Config source: https://git.io/JesV9 * * Feel free to let us know via PR, if you find something broken in this config * file. */ import Env from '@ioc:Adonis/Core/Env' import type { DatabaseConfig } from '@ioc:Adonis/Lucid/Database' const databaseConfig: DatabaseConfig = { /* |-------------------------------------------------------------------------- | Connection |-------------------------------------------------------------------------- | | The primary connection for making database queries across the application | You can use any key from the `connections` object defined in this same | file. | */ connection: Env.get('DB_CONNECTION'), connections: { /* |-------------------------------------------------------------------------- | MySQL config |-------------------------------------------------------------------------- | | Configuration for MySQL database. Make sure to install the driver | from npm when using this connection | | npm i mysql2 | */ mysql: { client: 'mysql2', connection: { host: Env.get('MYSQL_HOST'), port: Env.get('MYSQL_PORT'), user: Env.get('MYSQL_USER'), password: Env.get('MYSQL_PASSWORD', ''), database: Env.get('MYSQL_DB_NAME'), }, migrations: { naturalSort: true, }, healthCheck: false, debug: false, }, } } export default databaseConfig ================================================ FILE: config/drive.ts ================================================ /** * Config source: https://git.io/JBt3o * * Feel free to let us know via PR, if you find something broken in this config * file. */ import Env from '@ioc:Adonis/Core/Env' import { driveConfig } from '@adonisjs/core/build/config' import Application from '@ioc:Adonis/Core/Application' /* |-------------------------------------------------------------------------- | Drive Config |-------------------------------------------------------------------------- | | The `DriveConfig` relies on the `DisksList` interface which is | defined inside the `contracts` directory. | */ export default driveConfig({ /* |-------------------------------------------------------------------------- | Default disk |-------------------------------------------------------------------------- | | The default disk to use for managing file uploads. The value is driven by | the `DRIVE_DISK` environment variable. | */ disk: Env.get('DRIVE_DISK'), disks: { /* |-------------------------------------------------------------------------- | Local |-------------------------------------------------------------------------- | | Uses the local file system to manage files. Make sure to turn off serving | files when not using this disk. | */ local: { driver: 'local', visibility: 'public', /* |-------------------------------------------------------------------------- | Storage root - Local driver only |-------------------------------------------------------------------------- | | Define an absolute path to the storage directory from where to read the | files. | */ root: Application.tmpPath('uploads'), /* |-------------------------------------------------------------------------- | Serve files - Local driver only |-------------------------------------------------------------------------- | | When this is set to true, AdonisJS will configure a files server to serve | files from the disk root. This is done to mimic the behavior of cloud | storage services that has inbuilt capabilities to serve files. | */ serveFiles: true, /* |-------------------------------------------------------------------------- | Base path - Local driver only |-------------------------------------------------------------------------- | | Base path is always required when "serveFiles = true". Also make sure | the `basePath` is unique across all the disks using "local" driver and | you are not registering routes with this prefix. | */ basePath: '/uploads', }, /* |-------------------------------------------------------------------------- | S3 Driver |-------------------------------------------------------------------------- | | Uses the S3 cloud storage to manage files. Make sure to install the s3 | drive separately when using it. | |************************************************************************** | npm i @adonisjs/drive-s3 |************************************************************************** | */ // s3: { // driver: 's3', // visibility: 'public', // key: Env.get('S3_KEY'), // secret: Env.get('S3_SECRET'), // region: Env.get('S3_REGION'), // bucket: Env.get('S3_BUCKET'), // endpoint: Env.get('S3_ENDPOINT'), // // // For minio to work // // forcePathStyle: true, // }, /* |-------------------------------------------------------------------------- | GCS Driver |-------------------------------------------------------------------------- | | Uses the Google cloud storage to manage files. Make sure to install the GCS | drive separately when using it. | |************************************************************************** | npm i @adonisjs/drive-gcs |************************************************************************** | */ // gcs: { // driver: 'gcs', // visibility: 'public', // keyFilename: Env.get('GCS_KEY_FILENAME'), // bucket: Env.get('GCS_BUCKET'), /* |-------------------------------------------------------------------------- | Uniform ACL - Google cloud storage only |-------------------------------------------------------------------------- | | When using the Uniform ACL on the bucket, the "visibility" option is | ignored. Since, the files ACL is managed by the google bucket policies | directly. | |************************************************************************** | Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access |************************************************************************** | | The following option just informs drive whether your bucket is using uniform | ACL or not. The actual setting needs to be toggled within the Google cloud | console. | */ // usingUniformAcl: false, // }, }, }) ================================================ FILE: config/hash.ts ================================================ /** * Config source: https://git.io/JfefW * * Feel free to let us know via PR, if you find something broken in this config * file. */ import Env from '@ioc:Adonis/Core/Env' import { hashConfig } from '@adonisjs/core/build/config' /* |-------------------------------------------------------------------------- | Hash Config |-------------------------------------------------------------------------- | | The `HashConfig` relies on the `HashList` interface which is | defined inside `contracts` directory. | */ export default hashConfig({ /* |-------------------------------------------------------------------------- | Default hasher |-------------------------------------------------------------------------- | | By default we make use of the argon hasher to hash values. However, feel | free to change the default value | */ default: Env.get('HASH_DRIVER', 'scrypt'), list: { /* |-------------------------------------------------------------------------- | scrypt |-------------------------------------------------------------------------- | | Scrypt mapping uses the Node.js inbuilt crypto module for creating | hashes. | | We are using the default configuration recommended within the Node.js | documentation. | https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback | */ scrypt: { driver: 'scrypt', cost: 16384, blockSize: 8, parallelization: 1, saltSize: 16, keyLength: 64, maxMemory: 32 * 1024 * 1024, }, /* |-------------------------------------------------------------------------- | Argon |-------------------------------------------------------------------------- | | Argon mapping uses the `argon2` driver to hash values. | | Make sure you install the underlying dependency for this driver to work. | https://www.npmjs.com/package/phc-argon2. | | npm install phc-argon2 | */ argon: { driver: 'argon2', variant: 'id', iterations: 3, memory: 4096, parallelism: 1, saltSize: 16, }, /* |-------------------------------------------------------------------------- | Bcrypt |-------------------------------------------------------------------------- | | Bcrypt mapping uses the `bcrypt` driver to hash values. | | Make sure you install the underlying dependency for this driver to work. | https://www.npmjs.com/package/phc-bcrypt. | | npm install phc-bcrypt | */ bcrypt: { driver: 'bcrypt', rounds: 10, }, }, }) ================================================ FILE: contracts/ally.ts ================================================ /** * Contract source: https://git.io/JOdiQ * * Feel free to let us know via PR, if you find something broken in this contract * file. */ declare module '@ioc:Adonis/Addons/Ally' { interface SocialProviders { spotify: { config: SpotifyDriverConfig implementation: SpotifyDriverContract } } } ================================================ FILE: contracts/auth.ts ================================================ /** * Contract source: https://git.io/JOdz5 * * Feel free to let us know via PR, if you find something broken in this * file. */ import User from 'App/Models/User' declare module '@ioc:Adonis/Addons/Auth' { /* |-------------------------------------------------------------------------- | Providers |-------------------------------------------------------------------------- | | The providers are used to fetch users. The Auth module comes pre-bundled | with two providers that are `Lucid` and `Database`. Both uses database | to fetch user details. | | You can also create and register your own custom providers. | */ interface ProvidersList { /* |-------------------------------------------------------------------------- | User Provider |-------------------------------------------------------------------------- | | The following provider uses Lucid models as a driver for fetching user | details from the database for authentication. | | You can create multiple providers using the same underlying driver with | different Lucid models. | */ user: { implementation: LucidProviderContract config: LucidProviderConfig } } /* |-------------------------------------------------------------------------- | Guards |-------------------------------------------------------------------------- | | The guards are used for authenticating users using different drivers. | The auth module comes with 3 different guards. | | - SessionGuardContract | - BasicAuthGuardContract | - OATGuardContract ( Opaque access token ) | | Every guard needs a provider for looking up users from the database. | */ interface GuardsList { /* |-------------------------------------------------------------------------- | OAT Guard |-------------------------------------------------------------------------- | | OAT, stands for (Opaque access tokens) guard uses database backed tokens | to authenticate requests. | */ api: { implementation: OATGuardContract<'user', 'api'> config: OATGuardConfig<'user'> client: OATClientContract<'user'> } } } ================================================ FILE: contracts/drive.ts ================================================ /** * Contract source: https://git.io/JBt3I * * Feel free to let us know via PR, if you find something broken in this contract * file. */ import type { InferDisksFromConfig } from '@adonisjs/core/build/config' import type driveConfig from '../config/drive' declare module '@ioc:Adonis/Core/Drive' { interface DisksList extends InferDisksFromConfig {} } ================================================ FILE: contracts/env.ts ================================================ /** * Contract source: https://git.io/JTm6U * * Feel free to let us know via PR, if you find something broken in this contract * file. */ declare module '@ioc:Adonis/Core/Env' { /* |-------------------------------------------------------------------------- | Getting types for validated environment variables |-------------------------------------------------------------------------- | | The `default` export from the "../env.ts" file exports types for the | validated environment variables. Here we merge them with the `EnvTypes` | interface so that you can enjoy intellisense when using the "Env" | module. | */ type CustomTypes = typeof import('../env').default interface EnvTypes extends CustomTypes { } } ================================================ FILE: contracts/events.ts ================================================ /** * Contract source: https://git.io/JfefG * * Feel free to let us know via PR, if you find something broken in this contract * file. */ declare module '@ioc:Adonis/Core/Event' { /* |-------------------------------------------------------------------------- | Define typed events |-------------------------------------------------------------------------- | | You can define types for events inside the following interface and | AdonisJS will make sure that all listeners and emit calls adheres | to the defined types. | | For example: | | interface EventsList { | 'new:user': UserModel | } | | Now calling `Event.emit('new:user')` will statically ensure that passed value is | an instance of the the UserModel only. | */ interface EventsList { // } } ================================================ FILE: contracts/hash.ts ================================================ /** * Contract source: https://git.io/Jfefs * * Feel free to let us know via PR, if you find something broken in this contract * file. */ import type { InferListFromConfig } from '@adonisjs/core/build/config' import type hashConfig from '../config/hash' declare module '@ioc:Adonis/Core/Hash' { interface HashersList extends InferListFromConfig {} } ================================================ FILE: contracts/tests.ts ================================================ /** * Contract source: https://bit.ly/3DP1ypf * * Feel free to let us know via PR, if you find something broken in this contract * file. */ import '@japa/runner' declare module '@japa/runner' { interface TestContext { // Extend context } interface Test { // Extend test } } ================================================ FILE: database/factories/index.ts ================================================ // import Factory from '@ioc:Adonis/Lucid/Factory' ================================================ FILE: database/migrations/1698432448829_users.ts ================================================ import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class extends BaseSchema { protected tableName = 'users' public async up() { this.schema.createTable(this.tableName, (table) => { table.increments('id').primary() table.string('name').notNullable() table.string('email', 255).notNullable().unique() // table.string('password', 180).notNullable() table.string('remember_me_token').nullable() table.string('access_token') table.string('provider') /** * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('created_at', { useTz: true }).notNullable() table.timestamp('updated_at', { useTz: true }).notNullable() }) } public async down() { this.schema.dropTable(this.tableName) } } ================================================ FILE: database/migrations/1698432448832_api_tokens.ts ================================================ import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class extends BaseSchema { protected tableName = 'api_tokens' public async up() { this.schema.createTable(this.tableName, (table) => { table.increments('id').primary() table.integer('user_id').unsigned() table.string('name').notNullable() table.string('type').notNullable() table.string('token', 64).notNullable().unique() /** * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('expires_at', { useTz: true }).nullable() table.timestamp('created_at', { useTz: true }).notNullable() }) } public async down() { this.schema.dropTable(this.tableName) } } ================================================ FILE: database/migrations/1698440823891_social_tokens.ts ================================================ import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class extends BaseSchema { protected tableName = 'social_tokens' public async up () { this.schema.createTable(this.tableName, (table) => { table.increments('id') table.integer('user_id').unsigned() table.text('token').notNullable() table.text('refresh_token').notNullable() table.dateTime('expires_at').notNullable() table.string('type', 50).notNullable() /** * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('created_at', { useTz: true }) table.timestamp('updated_at', { useTz: true }) }) } public async down () { this.schema.dropTable(this.tableName) } } ================================================ FILE: database/migrations/1698491899562_genders.ts ================================================ import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class extends BaseSchema { protected tableName = 'genders' public async up () { this.schema.createTable(this.tableName, (table) => { table.increments('id') table.string('name', 255) table.tinyint('status').defaultTo(1) /** * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('created_at', { useTz: true }) table.timestamp('updated_at', { useTz: true }) }) } public async down () { this.schema.dropTable(this.tableName) } } ================================================ FILE: database/migrations/1698491899563_profiles.ts ================================================ import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class extends BaseSchema { protected tableName = 'profiles' public async up () { this.schema.createTable(this.tableName, (table) => { table.increments('id') table.date('date_of_birth') table.text('description') table.string('avatar') table.integer('prefered_gender_id') .unsigned() table.integer('user_id') .unsigned() /** * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('created_at', { useTz: true }) table.timestamp('updated_at', { useTz: true }) }) } public async down () { this.schema.dropTable(this.tableName) } } ================================================ FILE: database/migrations/1698528332152_artists.ts ================================================ import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class extends BaseSchema { protected tableName = 'artists' public async up () { this.schema.createTable(this.tableName, (table) => { table.increments('id') table.integer('user_id') .unsigned() /* user => track || null */ table.string('type') table.string('name') table.string('popularity') table.string('followers') table.string('uri') table.string('spotify_artist_id') table.string('artist_image') /** * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('created_at', { useTz: true }) table.timestamp('updated_at', { useTz: true }) }) } public async down () { this.schema.dropTable(this.tableName) } } ================================================ FILE: database/migrations/1698570713652_genres.ts ================================================ import BaseSchema from '@ioc:Adonis/Lucid/Schema' // "genres": [ // "pakistani indie", // "pakistani pop", // "urdu hip hop" // ], export default class extends BaseSchema { protected tableName = 'genres' public async up () { this.schema.createTable(this.tableName, (table) => { table.increments('id') table.integer('artist_id') .unsigned() table.string('name') /** * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('created_at', { useTz: true }) table.timestamp('updated_at', { useTz: true }) }) } public async down () { this.schema.dropTable(this.tableName) } } ================================================ FILE: database/migrations/1698571837102_tracks.ts ================================================ import BaseSchema from "@ioc:Adonis/Lucid/Schema"; export default class extends BaseSchema { protected tableName = "tracks"; public async up() { this.schema.createTable(this.tableName, (table) => { table.increments("id"); table.integer("user_id").unsigned(); table.string("name"); table.string("uri"); table.string("popularity"); table.string("tract_image"); table.string("track_id"); table.string("album"); table.tinyint('favorite').defaultTo(0) /** * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp("created_at", { useTz: true }); table.timestamp("updated_at", { useTz: true }); }); } public async down() { this.schema.dropTable(this.tableName); } } ================================================ FILE: database/migrations/1698942906705_matches.ts ================================================ import BaseSchema from '@ioc:Adonis/Lucid/Schema' export default class extends BaseSchema { protected tableName = 'matches' public async up () { this.schema.createTable(this.tableName, (table) => { table.increments('id') table.integer('matcher_user_id') table.integer('matched_user_id') table.date('match_date') table.tinyint("mutual_match").defaultTo(0) /** * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL */ table.timestamp('created_at', { useTz: true }) table.timestamp('updated_at', { useTz: true }) }) } public async down () { this.schema.dropTable(this.tableName) } } ================================================ FILE: database/seeders/Gender.ts ================================================ import BaseSeeder from '@ioc:Adonis/Lucid/Seeder' import Gender from 'App/Models/Gender' export default class extends BaseSeeder { public async run () { await Gender.createMany([ { name: 'Male', }, { name: 'Female', }, { name: 'Other', }, ]) } } ================================================ FILE: env.ts ================================================ /* |-------------------------------------------------------------------------- | Validating Environment Variables |-------------------------------------------------------------------------- | | In this file we define the rules for validating environment variables. | By performing validation we ensure that your application is running in | a stable environment with correct configuration values. | | This file is read automatically by the framework during the boot lifecycle | and hence do not rename or move this file to a different location. | */ import Env from '@ioc:Adonis/Core/Env' export default Env.rules({ HOST: Env.schema.string({ format: 'host' }), PORT: Env.schema.number(), APP_KEY: Env.schema.string(), APP_NAME: Env.schema.string(), DRIVE_DISK: Env.schema.enum(['local'] as const), NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), DB_CONNECTION: Env.schema.string(), MYSQL_HOST: Env.schema.string({ format: 'host' }), MYSQL_PORT: Env.schema.number(), MYSQL_USER: Env.schema.string(), MYSQL_PASSWORD: Env.schema.string.optional(), MYSQL_DB_NAME: Env.schema.string(), SPOTIFY_CLIENT_ID: Env.schema.string(), SPOTIFY_CLIENT_SECRET: Env.schema.string(), SPOTIFY_CALLBACK_URL: Env.schema.string.optional(), SESSION_DRIVER: Env.schema.string() }) ================================================ FILE: package.json ================================================ { "name": "musical-matching", "version": "1.0.0", "private": true, "scripts": { "dev": "node ace serve --watch", "build": "node ace build --production", "start": "node server.js", "test": "node ace test" }, "devDependencies": { "@adonisjs/assembler": "^5.9.6", "@japa/preset-adonis": "^1.2.0", "@japa/runner": "^2.5.1", "@types/proxy-addr": "^2.0.2", "@types/source-map-support": "^0.5.9", "adonis-preset-ts": "^2.1.0", "pino-pretty": "^10.2.3", "typescript": "~4.6", "youch": "^3.3.2", "youch-terminal": "^2.2.3" }, "dependencies": { "@adonisjs/ally": "^4.1.5", "@adonisjs/auth": "^8.2.3", "@adonisjs/core": "^5.9.0", "@adonisjs/lucid": "^18.4.2", "@adonisjs/repl": "^3.1.11", "axios": "^1.6.0", "luxon": "^3.4.3", "mysql2": "^3.6.2", "proxy-addr": "^2.0.7", "reflect-metadata": "^0.1.13", "source-map-support": "^0.5.21" } } ================================================ FILE: providers/AppProvider.ts ================================================ import type { ApplicationContract } from '@ioc:Adonis/Core/Application' export default class AppProvider { constructor (protected app: ApplicationContract) { } public register () { // Register your own bindings } public async boot () { // IoC container is ready } public async ready () { // App is ready } public async shutdown () { // Cleanup, since app is going down } } ================================================ FILE: server.ts ================================================ /* |-------------------------------------------------------------------------- | AdonisJs Server |-------------------------------------------------------------------------- | | The contents in this file is meant to bootstrap the AdonisJs application | and start the HTTP server to accept incoming connections. You must avoid | making this file dirty and instead make use of `lifecycle hooks` provided | by AdonisJs service providers for custom code. | */ import 'reflect-metadata' import sourceMapSupport from 'source-map-support' import { Ignitor } from '@adonisjs/core/build/standalone' sourceMapSupport.install({ handleUncaughtExceptions: false }) new Ignitor(__dirname) .httpServer() .start() ================================================ FILE: start/kernel.ts ================================================ /* |-------------------------------------------------------------------------- | Application middleware |-------------------------------------------------------------------------- | | This file is used to define middleware for HTTP requests. You can register | middleware as a `closure` or an IoC container binding. The bindings are | preferred, since they keep this file clean. | */ import Server from '@ioc:Adonis/Core/Server' /* |-------------------------------------------------------------------------- | Global middleware |-------------------------------------------------------------------------- | | An array of global middleware, that will be executed in the order they | are defined for every HTTP requests. | */ Server.middleware.register([ () => import('@ioc:Adonis/Core/BodyParser'), ]) /* |-------------------------------------------------------------------------- | Named middleware |-------------------------------------------------------------------------- | | Named middleware are defined as key-value pair. The value is the namespace | or middleware function and key is the alias. Later you can use these | alias on individual routes. For example: | | { auth: () => import('App/Middleware/Auth') } | | and then use it as follows | | Route.get('dashboard', 'UserController.dashboard').middleware('auth') | */ Server.middleware.registerNamed({ auth: () => import('App/Middleware/Auth') }) ================================================ FILE: start/routes.ts ================================================ /* |-------------------------------------------------------------------------- | Routes |-------------------------------------------------------------------------- | | This file is dedicated for defining HTTP routes. A single file is enough | for majority of projects, however you can define routes in different | files and just make sure to import them inside this file. For example | | Define routes in following two files | ├── start/routes/cart.ts | ├── start/routes/customer.ts | | and then import them inside `start/routes.ts` as follows | | import './routes/cart' | import './routes/customer' | */ import Route from "@ioc:Adonis/Core/Route"; Route.get('health', ({ response }) => response.noContent()) Route.group(() => { Route.get("/", async ({ response }) => { return response.json("Working."); }); // SIGN IN ROUTES Route.get("/signin", "UsersController.redirect"); //OAuth CALLBACK Route.get("/signin-callback", "UsersController.handleCallback"); Route.post("/logout", "UsersController.logout"); Route.group(() => { Route.get("/", "ProfilesController.get"); Route.post("/", "ProfilesController.store"); }) .prefix("profile") .middleware("auth:api"); Route.group(() => { Route.get("/", "GendersController.index"); }) .prefix("genders") .middleware("auth:api"); Route.group(() => { Route.get("/artists", "SpotifyController.artists"); Route.get("/tracks", "SpotifyController.tracks"); Route.get("/track-by-name", "SpotifyController.trackByName"); }) .prefix("spotify") .middleware("auth:api"); Route.group(() => { /* potential matches */ Route.get("/", "MatchesController.get"); /* mark match */ Route.post("/mutual", "MatchesController.mutualMatch"); /* get mutual match history */ Route.get("/history", "MatchesController.history"); }) .prefix("matches") .middleware("auth:api"); }).prefix("api"); ================================================ FILE: test.ts ================================================ /* |-------------------------------------------------------------------------- | Tests |-------------------------------------------------------------------------- | | The contents in this file boots the AdonisJS application and configures | the Japa tests runner. | | For the most part you will never edit this file. The configuration | for the tests can be controlled via ".adonisrc.json" and | "tests/bootstrap.ts" files. | */ process.env.NODE_ENV = 'test' import 'reflect-metadata' import sourceMapSupport from 'source-map-support' import { Ignitor } from '@adonisjs/core/build/standalone' import { configure, processCliArgs, run, RunnerHooksHandler } from '@japa/runner' sourceMapSupport.install({ handleUncaughtExceptions: false }) const kernel = new Ignitor(__dirname).kernel('test') kernel .boot() .then(() => import('./tests/bootstrap')) .then(({ runnerHooks, ...config }) => { const app: RunnerHooksHandler[] = [() => kernel.start()] configure({ ...kernel.application.rcFile.tests, ...processCliArgs(process.argv.slice(2)), ...config, ...{ importer: (filePath) => import(filePath), setup: app.concat(runnerHooks.setup), teardown: runnerHooks.teardown, }, cwd: kernel.application.appRoot }) run() }) ================================================ FILE: tests/bootstrap.ts ================================================ /** * File source: https://bit.ly/3ukaHTz * * Feel free to let us know via PR, if you find something broken in this contract * file. */ import type { Config } from '@japa/runner' import TestUtils from '@ioc:Adonis/Core/TestUtils' import { assert, runFailedTests, specReporter, apiClient } from '@japa/preset-adonis' /* |-------------------------------------------------------------------------- | Japa Plugins |-------------------------------------------------------------------------- | | Japa plugins allows you to add additional features to Japa. By default | we register the assertion plugin. | | Feel free to remove existing plugins or add more. | */ export const plugins: Required['plugins'] = [assert(), runFailedTests(), apiClient()] /* |-------------------------------------------------------------------------- | Japa Reporters |-------------------------------------------------------------------------- | | Japa reporters displays/saves the progress of tests as they are executed. | By default, we register the spec reporter to show a detailed report | of tests on the terminal. | */ export const reporters: Required['reporters'] = [specReporter()] /* |-------------------------------------------------------------------------- | Runner hooks |-------------------------------------------------------------------------- | | Runner hooks are executed after booting the AdonisJS app and | before the test files are imported. | | You can perform actions like starting the HTTP server or running migrations | within the runner hooks | */ export const runnerHooks: Pick, 'setup' | 'teardown'> = { setup: [() => TestUtils.ace().loadCommands()], teardown: [], } /* |-------------------------------------------------------------------------- | Configure individual suites |-------------------------------------------------------------------------- | | The configureSuite method gets called for every test suite registered | within ".adonisrc.json" file. | | You can use this method to configure suites. For example: Only start | the HTTP server when it is a functional suite. */ export const configureSuite: Required['configureSuite'] = (suite) => { if (suite.name === 'functional') { suite.setup(() => TestUtils.httpServer().start()) } } ================================================ FILE: tests/functional/hello_world.spec.ts ================================================ import { test } from '@japa/runner' test('display welcome page', async ({ client }) => { const response = await client.get('/') response.assertStatus(200) response.assertBodyContains({ hello: 'world' }) }) ================================================ FILE: tsconfig.json ================================================ { "extends": "adonis-preset-ts/tsconfig.json", "include": [ "**/*" ], "exclude": [ "node_modules", "build" ], "compilerOptions": { "outDir": "build", "rootDir": "./", "sourceMap": true, "paths": { "App/*": [ "./app/*" ], "Config/*": [ "./config/*" ], "Contracts/*": [ "./contracts/*" ], "Database/*": [ "./database/*" ] }, "types": [ "@adonisjs/core", "@adonisjs/repl", "@japa/preset-adonis/build/adonis-typings", "@adonisjs/lucid", "@adonisjs/ally", "@adonisjs/auth" ] } }