Sistema web para criação e gerenciamento de anotações multimídia. Além de texto, cada anotação pode ter imagens e áudios anexados, além de categoria, prioridade e data de lembrete. Cada usuário acessa apenas sua conta, protegida por autenticação JWT.
O projeto é dividido em uma API REST em ASP.NET Core e um frontend web em HTML, CSS e JavaScript puro, orquestrados com Docker Compose junto de um banco MySQL.
O objetivo foi construir uma aplicação completa de ponta a ponta, do banco de dados à interface, exercitando:
- API REST em camadas (Controllers → Services → Repositories) com injeção de dependência
- Autenticação e cadastro com ASP.NET Core Identity + JWT
- Mapeamento entre entidades e DTOs com AutoMapper
- Upload, armazenamento e streaming de arquivos (imagens e áudios) com validação de tipo e tamanho
- Modelagem relacional com Entity Framework Core (Usuário → Anotações → Arquivos de mídia, com exclusão em cascata)
- Frontend em JavaScript modular (ES Modules) consumindo a API com
fetch, tratamento de sessão expirada e logout automático - Containerização do backend, do frontend (Nginx) e do banco com Docker Compose
Adicione aqui as capturas de tela do projeto. Sugestão de telas:
- Cadastro e login de usuários com JWT (expiração configurável)
- Criação, listagem, edição e exclusão de anotações com título, conteúdo, categoria, prioridade e lembrete
- Prioridade em três níveis (Baixa, Média, Alta), com destaque por cor na listagem (verde, laranja e vermelho) e ordenação da mais alta para a mais baixa
- Anexos de mídia em cada anotação:
- Imagens:
JPG,JPEG,PNG,GIF,BMP - Áudios:
MP3,WAV,OGG,M4A - Até 50 MB por arquivo
- Imagens:
- Pré-visualização dos arquivos antes do envio, com opção de remover; imagens exibidas em miniatura e áudios com player embutido
- Edição de anotação com possibilidade de adicionar novos arquivos (inclusive arrastando e soltando) e excluir os já anexados
- Arquivos protegidos: só são servidos para requisições autenticadas
- Sessão expirada ou token inválido leva ao logout automático e redireciona para o login
- Layout responsivo, adaptado a telas largas e estreitas
- Documentação interativa da API com Swagger
Backend
- C# / .NET 9 (ASP.NET Core Web API)
- Entity Framework Core
- MySQL 8
- ASP.NET Core Identity
- Autenticação JWT Bearer
- AutoMapper
- Swagger (OpenAPI)
- Docker
Frontend
- HTML5, CSS3 e JavaScript (ES Modules), sem frameworks
fetchAPI para consumo da REST APIlocalStoragepara armazenamento do token JWT- Docker + Nginx (servidor da versão em container)
.
├── docker-compose.yml
├── MultimediaNotes.sln
├── MultimediaNotes.API/ # Backend (ASP.NET Core)
│ ├── Context/ # AppDbContext (EF Core + Identity)
│ ├── Controllers/ # Auth, User, Annotation, MediaFile
│ ├── DTOs/ # DTOs e MappingProfile (AutoMapper)
│ ├── Models/ # Entidades e Repositories
│ ├── Services/ # Regras de negócio e JWT
│ ├── appsettings.json
│ ├── Program.cs
│ └── Dockerfile
└── MultimediaNotes.FrontEnd/ # Frontend (HTML/CSS/JS)
├── CSS/ # style.css, login.css
├── JS/ # auth.js, api.js, mediaFileApi.js, notes.js, create.js, login.js, create_user.js
├── login.html
├── create_user.html
├── notes.html
├── create.html
├── default.conf # Configuração do Nginx
└── Dockerfile
- Docker e Docker Compose — necessário para o jeito recomendado de rodar o projeto
- Para rodar sem Docker (desenvolvimento local):
- .NET 9 SDK
- MySQL 8 (pode ser o container do próprio
docker-compose) - Ferramenta do EF Core:
dotnet tool install --global dotnet-ef
Os valores sensíveis ficam em dois lugares: no appsettings.json da API e no docker-compose.yml. Troque os valores de exemplo pelos seus e nunca versione senhas ou chaves reais.
MultimediaNotes.API/appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MultimediaNotes;Uid=root;Pwd=SUA_SENHA_DO_MYSQL"
},
"JwtSettings": {
"SecretKey": "CRIE_UMA_CHAVE_LONGA_E_ALEATORIA_AQUI",
"Issuer": "MultimediaNotes.API",
"Audience": "MultimediaNotes.Client",
"ExpirationHours": 24
}
}| Chave | Descrição |
|---|---|
ConnectionStrings:DefaultConnection |
String de conexão com o MySQL |
JwtSettings:SecretKey |
Chave usada para assinar os tokens JWT (use 32 caracteres ou mais, aleatórios) |
JwtSettings:Issuer / Audience |
Emissor e público do token |
JwtSettings:ExpirationHours |
Tempo de validade do token, em horas |
Para gerar uma chave aleatória:
# Linux / macOS / Git Bash
openssl rand -base64 48
# PowerShell
[System.Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(48))Qualquer configuração do
appsettings.jsonpode ser sobrescrita por variáveis de ambiente usando__como separador, por exemploConnectionStrings__DefaultConnectioneJwtSettings__SecretKey. É o que odocker-compose.ymlfaz com a string de conexão, apontandoServer=mysql(nome do serviço do banco na rede Docker).
A mesma senha do MySQL precisa aparecer em dois pontos do arquivo, o serviço backend e o serviço mysql:
backend:
environment:
- ConnectionStrings__DefaultConnection=Server=mysql;Database=MultimediaNotes;Uid=root;Pwd=SUA_SENHA_DO_MYSQL
mysql:
environment:
MYSQL_ROOT_PASSWORD: SUA_SENHA_DO_MYSQL
MYSQL_DATABASE: MultimediaNotesDica: para não deixar a senha escrita no arquivo, crie um
.envna raiz (já ignorado pelo.dockerignore) e referencie com${MYSQL_ROOT_PASSWORD}nodocker-compose.yml.
A API não aplica migrations sozinha ao iniciar. Com o MySQL no ar, crie as tabelas uma vez a partir da pasta da API:
cd MultimediaNotes.API
dotnet ef database updateRodando a partir da sua máquina, a conexão usa o
Server=localhostdoappsettings.json. O container do MySQL já expõe a porta3306para isso.
Sobe o banco, a API e o frontend com um único comando:
docker-compose up -d --buildDepois de subir os containers (e criar as tabelas, conforme a seção anterior), acesse:
| Serviço | URL |
|---|---|
| Frontend | http://127.0.0.1:3000 |
| API + Swagger | http://localhost:5145 |
| MySQL | localhost:3306 |
⚠️ Acesse o frontend por127.0.0.1, e não porlocalhost. A política de CORS da API libera somente a origemhttp://127.0.0.1:3000, então abrir porlocalhost:3000faz as requisições serem bloqueadas pelo navegador.
Os arquivos enviados ficam na pasta
uploads/dentro do container da API. Para não perdê-los ao recriar o container, monte um volume para esse diretório nodocker-compose.yml.
1. Suba apenas o banco:
docker-compose up -d mysql2. Aplique as migrations e rode a API:
cd MultimediaNotes.API
dotnet ef database update
dotnet run --launch-profile httpA API sobe em http://localhost:5145, com o Swagger na raiz.
3. Sirva o frontend na porta 3000 (qualquer servidor estático serve, desde que seja a partir da raiz da pasta e em 127.0.0.1):
cd MultimediaNotes.FrontEnd
python -m http.server 3000 --bind 127.0.0.1Acesse http://127.0.0.1:3000/login.html.
A URL da API (
http://localhost:5145) está escrita diretamente nos arquivosauth.js,api.js,mediaFileApi.js,create.jsenotes.js. Se mudar a porta ou o host do backend, atualize esses arquivos.
Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:3306 ...
bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
Causa: já existe um MySQL rodando na sua máquina (por exemplo, instalado como serviço do Windows) usando a porta 3306, e o container do banco tenta usar a mesma porta. Os containers da API e do frontend chegam a ser criados, mas o banco não sobe e a aplicação não funciona.
Para confirmar quem está usando a porta (no Windows):
netstat -ano | findstr :3306Há duas formas de resolver:
Opção A — Parar o MySQL local. Em um terminal como administrador (o nome do serviço pode variar, confira em services.msc):
net stop MySQL80
docker-compose up -dOpção B — Mudar a porta do banco no Docker. No docker-compose.yml, troque o mapeamento do serviço mysql para uma porta livre do seu computador, por exemplo 3307:
mysql:
ports:
- "3307:3306"A API não é afetada, pois dentro da rede do Docker ela continua acessando o banco em mysql:3306. A mudança só vale para quem acessa o banco de fora do Docker, como o dotnet ef database update rodado na sua máquina. Nesse caso, informe a porta na string de conexão do appsettings.json:
Server=localhost;Port=3307;Database=MultimediaNotes;Uid=root;Pwd=SUA_SENHA_DO_MYSQL
Depois de resolver, suba novamente com docker-compose up -d.
O mesmo tipo de conflito pode ocorrer com as portas
3000(frontend) e5145(API) se outro programa as estiver usando. A solução é a mesma: liberar a porta ou alterar o mapeamentoporta_da_sua_máquina:porta_do_containernodocker-compose.yml. Se mudar a porta da API, atualize também a URL nos arquivos.jsdo frontend, e se mudar a do frontend, ajuste a origem permitida no CORS doProgram.cs.
Se a API responde 400 com a mensagem Erro interno do servidor ao cadastrar um usuário, ela provavelmente não está conseguindo acessar o banco. Verifique:
- O MySQL está rodando? Rode
docker-compose pse confira se o serviçomysqlestárunning(ouhealthy). - As tabelas foram criadas? Se o banco está vazio, aplique as migrations:
cd MultimediaNotes.API
dotnet ef database updatePara ver o erro real, consulte os logs da API:
docker-compose logs backendAs migrations só precisam ser aplicadas uma vez. Os dados ficam guardados no volume
mysql_datae permanecem entre reinicializações. Só será necessário aplicá-las de novo se o volume for apagado (docker-compose down -v).
Não existe usuário padrão. Na tela de login, clique em Criar conta e cadastre-se com nome, e-mail e senha. A senha deve ter:
- no mínimo 6 caracteres
- pelo menos uma letra maiúscula, uma minúscula e um número
Após 5 tentativas de login incorretas, a conta fica bloqueada por 5 minutos.
Todas as rotas, exceto login e register, exigem o header Authorization: Bearer <token>.
| Método | Rota | Descrição |
|---|---|---|
| POST | /login |
Autentica e retorna o token JWT |
| POST | /register |
Cria uma conta e retorna o token JWT |
| GET | /validate |
Verifica se o token atual é válido |
| Método | Rota | Descrição |
|---|---|---|
| GET | / |
Lista todas as anotações |
| GET | /{id} |
Busca uma anotação pelo id |
| GET | /user/{userId} |
Lista as anotações de um usuário |
| POST | / |
Cria uma anotação |
| PUT | / |
Atualiza uma anotação (o id vai no corpo) |
| DELETE | /{id} |
Exclui uma anotação e seus arquivos |
| Método | Rota | Descrição |
|---|---|---|
| GET | /annotation/{annotationId} |
Lista os arquivos de uma anotação |
| GET | /{id} |
Baixa/transmite o arquivo |
| POST | /upload |
Envia um arquivo (multipart/form-data com os campos File e AnnotationId) |
| DELETE | /{id} |
Exclui o arquivo |
| Método | Rota | Descrição |
|---|---|---|
| GET | / |
Lista os usuários |
| GET | /annotations |
Lista os usuários com suas anotações |
| GET | /{id} |
Busca um usuário pelo id |
| POST | / |
Cria um usuário |
| PUT | /{id} |
Atualiza um usuário |
| DELETE | /{id} |
Exclui um usuário |
User (1) ────< Annotation (1) ────< MediaFile
| Entidade | Campos principais |
|---|---|
| User | Id, Name, Email + campos do ASP.NET Identity |
| Annotation | Id, Title (até 50), Content, Category (até 50), Priority (1 a 3), Reminder, UserId |
| MediaFile | Id, FileName, FilePath, FileType (image ou audio), MimeType, FileSize, CreatedAt, AnnotationId |
Excluir um usuário remove suas anotações, e excluir uma anotação remove seus arquivos (exclusão em cascata).
Este projeto foi desenvolvido com foco em aprendizado e não está pronto para ser colocado em produção. Alguns pontos de segurança e de arquitetura são conhecidos e ficaram como melhorias futuras:
Segurança
- Credenciais versionadas: o
appsettings.jsone odocker-compose.ymlcontêm a senha do MySQL e a chave JWT escritas diretamente, e permanecem no histórico do Git. Esses valores existem apenas para o ambiente de desenvolvimento deste projeto, e devem ser trocados (e movidos para variáveis de ambiente,.envoudotnet user-secrets) em qualquer uso real. - Falta de controle de propriedade: os endpoints de anotações e arquivos exigem login, mas não verificam se o recurso pertence ao usuário autenticado. Na prática, quem conhece o
idde uma anotação ou arquivo consegue consultá-lo ou alterá-lo. OuserIdda anotação também é enviado pelo cliente, em vez de ser lido do token. - Gerenciamento de usuários sem restrição: os endpoints de
/api/Userpodem ser usados por qualquer usuário autenticado, não há perfis (como administrador). OPOST /api/Usercria o usuário com uma senha temporária fixa no código. - Token no
localStorage: o JWT é guardado nolocalStorage, que fica exposto caso exista alguma falha de XSS. Parte da interface é montada cominnerHTML, e valores controlados pelo usuário (como nomes de arquivo) precisam ser escapados. - Validação de uploads: os arquivos são validados pela extensão e pelo tamanho, e não pelo conteúdo real.
- Configuração de execução: o container da API roda com
ASPNETCORE_ENVIRONMENT=Development(Swagger exposto) e sem HTTPS configurado.
Arquitetura e qualidade
- As migrations são aplicadas manualmente, e a URL da API está escrita direto nos arquivos JavaScript do frontend.
- Os arquivos enviados ficam no disco do container, sem volume persistente.
- Os erros do
AuthServicesão capturados sem registro em log, o que dificulta o diagnóstico. - Não há testes automatizados.
Próximos passos planejados
- Remover credenciais do repositório (
appsettings.example.json+ variáveis de ambiente) e gerar uma nova chave JWT. - Ler o usuário a partir do token e validar a propriedade de anotações e arquivos em todos os endpoints.
- Restringir o gerenciamento de usuários a um perfil administrador.
- Adicionar logs, testes e um volume para os uploads.
Luís Henrique De Oliveira Ruppenthal GitHub




